diff --git a/applications/PoP/docs/gates_spikes_plan.md b/applications/PoP/docs/gates_spikes_plan.md index 8dfc452..2a2abd1 100644 --- a/applications/PoP/docs/gates_spikes_plan.md +++ b/applications/PoP/docs/gates_spikes_plan.md @@ -50,7 +50,7 @@ roomtest — живой прототип порта PoP: комната 1 уро - `pop_room_link(room, side)` — связь (side 0=L,1=R,2=U,3=D; 0=нет). - `pop_level_start_room/pos/dir()`. - `pop_bg.c/.h` — отрисовка тайлов (порт seg008 draw_tile), fore-окклюзия над - Kid (`pop_fore_over_kid`, порт set_char_collision+redraw_at_char/char2), + Kid (`pop_fore_over_char`, порт set_char_collision+redraw_at_char/char2), **loose-полы** (shake/bake/mob). `draw_tile` — статическая, знает `draw_gate_back` (грань гейта из левой комнаты). - `pop_kid.c/.h` — анимация Kid (интерпретатор seqtbl `play_seq`, порт seg006), diff --git a/applications/PoP/docs/host_tests_plan.md b/applications/PoP/docs/host_tests_plan.md index 1f3f101..ba6b7ed 100644 --- a/applications/PoP/docs/host_tests_plan.md +++ b/applications/PoP/docs/host_tests_plan.md @@ -40,7 +40,7 @@ ### 1.2 Журналирующий рендерер -Вместо `pop_bg.c`/`pop_gdraw.c` в тестовый бинарь линкуется модуль с теми +Вместо `pop_bg.c`/`pop_cdraw.c` в тестовый бинарь линкуется модуль с теми же прототипами, который **не рисует, а записывает вызовы**: какой тайл помечен к перерисовке, каким кодом, с каким счётчиком страниц. @@ -126,7 +126,7 @@ fore-клипа (`pop_fore_set_clip`) одно на всех, и его став рисует персонажа; когда порядок «Кид/страж» стал переменным, окно осталось стражьим, и Кид нарисовался поверх передних столбов. Это не «функция вернула не то», unit-тест такое не видит. Ловится инвариантом, -вкомпилированным в safe-сборку: «в момент `pop_fore_over_kid` окно клипа +вкомпилированным в safe-сборку: «в момент `pop_fore_over_char` окно клипа принадлежит Киду». Отдельный инструмент, дополняющий тесты. ## Порядок работ diff --git a/applications/PoP/docs/layout_plan_v2.md b/applications/PoP/docs/layout_plan_v2.md index eec9c54..3534add 100644 --- a/applications/PoP/docs/layout_plan_v2.md +++ b/applications/PoP/docs/layout_plan_v2.md @@ -1,8 +1,16 @@ # roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам -> **Замер 2026-08-01 (актуальная сборка).** `_CODE` 25 119 Б, `_DATA` 3 709, +> **Замер 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-01 (историческая отметка).** `_CODE` 25 119 Б, `_DATA` 3 709, > куча ~2.4 КБ. Банки: 1 (`guards.c`) 1 896, 2 (`pop_bg.c`) 13 792, -> 3 (`pop_map.c`) 6 331, 4 (`pop_gdraw.c`) 2 236 — все из 16 384. +> 3 (`pop_map.c`) 6 331, 4 (отрисовка стража) 2 236 — все из 16 384. > **Резидента `--w3` больше нет**: отрисовка уехала в банк 2, и это сняло > главное ограничение резидента (из банка его было не достать) — банк→банк > работает, трамплин сохраняет страницу окна на стеке. Отрисовка стража diff --git a/applications/PoP/roomtest/CLAUDE.md b/applications/PoP/roomtest/CLAUDE.md index cdee835..c3e3df6 100644 --- a/applications/PoP/roomtest/CLAUDE.md +++ b/applications/PoP/roomtest/CLAUDE.md @@ -60,18 +60,25 @@ Kid — `kid_data.h` (генерится `pop_extract_kid_data.py`). | Файл | Роль (порт SDLPoP) | |------|--------------------| | `roomtest.c` | Главный цикл: дабл-буфер (2 страницы + флип на vsync), tick→draw порядок слоёв. | -| `pop_bg.c/.h` | Отрисовка фона композицией тайлов (`seg008` draw_tile/wall_pattern), fore-окклюзия поверх Kid (`seg003` redraw_at_char), loose-полы (shake/bake/mob, `seg007/008`). | -| `pop_kid.c/.h` | Анимация/движение Kid: интерпретатор seqtbl `play_seq` + frame_table (`seg006`). | +| `pop_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_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`) поверх ``. | | `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). | ## Порядок слоёв в кадре (важно для окклюзии) -`roomtest.c` каждый тик рисует в СКРЫТУЮ страницу: `kid_heal` (стереть -прошлый кадр) → `kid_tick` → `pop_phys_tick` → `pop_loose_tick` (loose СЛОЙ -ФОНА — ДО Kid, чтобы Kid был поверх плиты) → `kid_draw` → `pop_fore_over_kid` -(передние грани тайлов поверх Kid) → `pop_room_clip_borders` (чистка бортов, -гейт по флагу — только в кадрах падения) → флип на vsync. +`roomtest.c` каждый тик рисует в СКРЫТУЮ страницу: `pop_char_heal` (стереть +прошлый кадр — по слоту на персонажа) → `kid_tick` → `pop_phys_tick` → +`pop_loose_tick` (loose СЛОЙ ФОНА — ДО персонажей, чтобы они были поверх +плиты) → `pop_char_draw`/`pop_char_fore` для соперника и Кида (кто позже — +тот поверх, порядок задаёт обход тайлов) → `pop_room_clip_borders` (чистка +бортов, гейт по флагу — только в кадрах падения) → флип на vsync. + +Отрисовка персонажа ОДНА на всех (`pop_cdraw.c`): слот выбирает окно +`Char`, набор атласов и heal-прямоугольники, всё остальное общее — как два +одинаковых входа `add_*_to_objtable` в оригинале. **Дабл-буфер:** у каждой страницы своя видео-ОЗУ и теневая ОЗУ-копия; heal берёт чистый фон из копии ТОЙ страницы, в которую рисуем. Любой динамический diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index ec50d7b..a2a17e3 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -6,7 +6,7 @@ EXAMPLE := roomtest # small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе # CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge). MEMORY ?= huge # small-раскладка + банки кода в W3 (стражи) -# Резидента --w3 БОЛЬШЕ НЕТ: отрисовка уехала в БАНК 2 (pop_bg + pop_gdraw +# Резидента --w3 БОЛЬШЕ НЕТ: отрисовка уехала в БАНК 2 (pop_bg + pop_cdraw # в одной странице — вызовы между ними прямые, если функция не помечена # __banked). Так снято главное ограничение резидента: из банка его было # не достать ни прямо, ни транзитивно, а банк→банк работает (трамплин @@ -35,14 +35,14 @@ MEMORY ?= huge # small-раскладка + банки кода в W # ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том # же ALLOCS — иначе сравниваются не правки, а уровни оптимизации. ALLOCS ?= 3000 -EXTRA_FLAGS ?= --gfx 256 --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 4=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c $(PROF_FLAGS) +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) # Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по # умолчанию, пока идёт работа с производительностью; make PROF=0 выключает. # Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте # MAME = время фазы в растровых строках. PROF ?= 1 PROF_FLAGS := -DPROF_BORDER=$(PROF) -EXTRA_SRCS := pop_state.c pop_draw.c pop_kid.c pop_level.c pop_redraw.c pop_geom.c pop_guard.c +EXTRA_SRCS := pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_redraw.c pop_geom.c pop_guard.c BG_DIR := $(CURDIR)/../poc/res/bg KID_DIR := $(CURDIR)/../poc/res/kid @@ -57,7 +57,7 @@ BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2. $(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl \ $(BG_DIR)/pop_pot.atl # ВСЕ 28 страниц Kid — иначе кадры торможения/разворота/приседа (image>23) -# не рисуются (kid_draw пропускает незагруженную страницу). Палитра — +# не рисуются (отрисовка пропускает незагруженную страницу). Палитра — # ЕДИНАЯ игровая (env+wall+kid). KID_PAGES := $(foreach n,0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27,$(KID_DIR)/kid$(n).atl) KID_DATA := $(KID_PAGES) $(KID_DIR)/kid.pal $(KID_DIR)/sword.atl @@ -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_gdraw.c pop_draw.c _pop_draw.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_kid.c pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA) +$(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) diff --git a/applications/PoP/roomtest/TASKS_CLOSED.md b/applications/PoP/roomtest/TASKS_CLOSED.md index a38f066..e67233e 100644 --- a/applications/PoP/roomtest/TASKS_CLOSED.md +++ b/applications/PoP/roomtest/TASKS_CLOSED.md @@ -1,4 +1,4 @@ -# roomtest — ЗАКРЫТЫЕ задачи (архив досок, обновлено 2026-08-07) +# roomtest — ЗАКРЫТЫЕ задачи (архив досок, обновлено 2026-08-08) Сделанное — с протоколами замеров, граблями и причинами решений. Файл существует не ради истории: половина записей ниже — это ЧИСЛА (сколько тактов @@ -232,6 +232,95 @@ BUG-1 (провал на row 1 при переходе через открыты ## Отрисовка и память + +### DRAW-CHAR. Отрисовка — ОДНА на всех Char — **СДЕЛАНО И ПРОВЕРЕНО 2026-08-08** + +> **Итог.** Отрисовка персонажа сведена к одному набору функций над `Char` +> (`pop_cdraw.c`, банк 4) — как физика после GUARD-PHYS. Было два +> независимых куска кода: `kid_draw`/`kid_heal`/`kid_draw_splash`/ +> `kid_fore_clip*` в резиденте W1 и `pop_guard_draw`/`pop_guard_heal` в +> банке 4, каждый со своей математикой кадра, своими heal-прямоугольниками +> и своим проходом окклюзии (`pop_fore_over_kid` / `pop_fore_over_char`). +> +> **Основание в оригинале** (сверено перед кодингом): `add_kid_to_objtable` +> (seg008:22F0) и `add_guard_to_objtable` (seg008:2324) имеют ИДЕНТИЧНОЕ +> тело и различаются окном (`loadkid`/`loadshad`), набором спрайтов и типом +> объекта; `redraw_at_char` / `redraw_at_char2` (seg003:0576/0645) гейтов по +> `charid` не имеют вовсе — единственная ветка по персонажу там это +> объединение с ПРОШЛЫМ футпринтом у Кида (пометки перерисовки, у нас их +> нет). `draw_hurt_splash` (seg006:2003) разводит Кида и остальных ровно +> двумя числами: chtab/image и подъём `((charid == kid) << 2) + 11`. + +**Что теперь одно на всех.** + +| Было (два места) | Стало | +|---|---| +| `kid_draw` / `pop_guard_draw` | `pop_char_draw(who)` | +| `kid_heal` / `pop_guard_heal` | `pop_char_heal(who)` | +| `kid_draw_splash` / ветка брызг внутри `pop_guard_draw` | `cd_splash` (порт целиком, все три ветки кадров) | +| `kid_fore_clip` + `kid_fore_clip_restore` / хвост `pop_guard_draw` | `pop_char_fore(who)` | +| `pop_fore_over_kid` / `pop_fore_over_char` | `pop_fore_over_char(ch, …)` | +| `kid_fp_*()` / `pop_guard_fp_width()` | `pop_cd[slot]` (структура в `_DATA`, читается из любого банка без трамплина) | +| `pop_kid_set_render_dx` | `pop_char_set_render_dx(who, dx)` | + +Слот (`POP_CH_KID` / `POP_CH_OPP`) выбирает ровно то, что различается в +оригинале: окно `Char`, набор атласов и кадр (`kidp`+`kid_frame` / +`gp`+`pop_gframe`), heal-прямоугольники по страницам дабл-буфера. + +**Что при этом ПОЧИНИЛОСЬ (расхождения, которые и были ценой дублирования).** + +1. **`clip_char` у соперника не было вовсе** — теперь общий (в оригинале он + в обоих `add_*_to_objtable`). +2. **Клип полем 192 у Кида не было** (`reset_obj_clip`: `obj_clip_bottom = + 192`): его спрайт рисовался на полосе HP, а следы за него подчищала + полоса `pop_room_clip_borders` по флагу `pop_clip_sprite`. Теперь + персонаж режется полем, как в оригинале, и `border_dirty` остался только + у падающей плиты. +3. **У Кида не было ветки брызг «мёртв / падение 106..110»** (`obj_y += 4`) + — была только у соперника. +4. **`char_width_half` СТРАЖА считался по спрайту КИДА**: `set_char_collision` + и `check_spike_below` (`pop_map.c`) читали `kid_fp_width()` безусловно, а + `pop_guard_fp_width()` использовался только для порядка отрисовки. + Теперь метрики берутся по `charid` активного персонажа (`CD_ACT`). +5. **Расширение перебора тайлов окном спрайта** (клинок и брызги уходят за + габарит кадра) было только у соперника — теперь общее. Заодно оно + перестало быть четырьмя лишними аргументами: окно fore-клипа уже лежит в + `pop_bg.c`, и проход читает его сам. + +**Цена / выигрыш (ALLOCS=3000, замер до и после):** + +``` + было стало дельта +_CODE (W1) 24 881 20 524 −4 357 ← куча 2 023 → 6 333 Б (+4 310) +BANK2 pop_bg 15 310 15 045 − 265 ← свободно 1 074 → 1 339 Б +BANK3 pop_map 8 608 8 713 + 105 (CD_ACT — индекс по charid) +BANK4 pop_cdraw 4 625 5 905 +1 280 (сюда переехала отрисовка Кида) +итого −3 237 +``` + +То есть слияние двух копий дало ~3.2 КБ, но **главный выигрыш не в банке 2, +а в резиденте**: отрисовка Кида уехала из W1 в банк, и куча выросла втрое +(2 023 → 6 333 Б). Банку 2 при этом досталось всего +265 Б свободного +места — **под [L3-CHOMP](TASKS_OPEN.md#l3-chomp) этого мало**, разгрузку +`pop_bg` придётся делать отдельно (свободные номера банков — 7+). + +**Проверено:** `tests-host` — все 5 наборов зелёные, трассы физики не +изменились (`[phys] ok: 1723`, тот же эталон; `[char] ok: 65`). В MAME +(свежий образ, полный рестарт): уровень 1 — комната 1 (Кид, кладка, +факелы, окклюзия колонны), падение на нижний ярус, комната 3 — страж своего +цвета с клинком и своей полосой HP, бой и смерть Кида. Артефактов +отрисовки и мусора на полосе HP нет. + +**Приёмка пользователем пройдена 2026-08-08.** Прогон вживую: отрисовка +персонажей, окклюзия и бой — без регрессий. + +Единственное наблюдение — **циан-полоса профиля (спрайты + fore) выросла**: +проход над Кидом теперь расширяется окном спрайта, как раньше только у +соперника, то есть перебирает на колонку-другую больше. Решение +пользователя: **в пределах допустимого, оптимизация — ОТДЕЛЬНОЙ задачей** +([DRAW-COST](TASKS_OPEN.md#draw-cost)). + + ### CLIP-1. Аудит блитов: где клип не нужен — **СДЕЛАНО 2026-08-01** diff --git a/applications/PoP/roomtest/TASKS_OPEN.md b/applications/PoP/roomtest/TASKS_OPEN.md index 4524b18..578f3e1 100644 --- a/applications/PoP/roomtest/TASKS_OPEN.md +++ b/applications/PoP/roomtest/TASKS_OPEN.md @@ -1,4 +1,4 @@ -# roomtest — доска ОТКРЫТЫХ задач (обновлено 2026-08-07) +# roomtest — доска ОТКРЫТЫХ задач (обновлено 2026-08-08) Что берём в работу сейчас и в каком порядке. Каждая запись: что сделать, почему именно сейчас, чем подтверждать результат. @@ -12,9 +12,18 @@ **Состояние на 2026-08-07:** пользователь прогнал ВСЕ комнаты уровней 1 и 2 — крупных багов нет. Приёмки [L1-PASS](TASKS_CLOSED.md#l1-pass) и [L2-PASS](TASKS_CLOSED.md#l2-pass) закрыты; с прогона открыты три записи по -уровню 2 (цвет стража, брызги по стражу, чит `+`/`−` в бою) — все в -[`bug_list.md`](bug_list.md). Уровень 3 играется, но приёмки не было: сначала -чомперы и скелет. +уровню 2, из них цвет стража и брызги уже закрыты (см. +[`bug_closed.md`](bug_closed.md)); открытым остался чит `+`/`−` в бою. +Уровень 3: **скелет сделан и проверен в MAME 2026-08-07** (бой, падение в +пропасть, окклюзия), чомперов ещё нет. + +**Разгрузка банка 2 сделана 2026-08-08** ([MEM-BANK2](#mem-bank2)): 90.4 % → +**72.9 %, свободно 4442 Б** — чомперам места хватает. + +**DRAW-CHAR сделана и ПРОВЕРЕНА 2026-08-08** (протокол и замеры — +[`TASKS_CLOSED.md`](TASKS_CLOSED.md#draw-char)): отрисовка теперь одна на +всех `Char`. Банку 2 она дала всего +265 Б — место под чомперов дала уже +[MEM-BANK2](#mem-bank2). Правило проекта в силе: механику сверять с `../SDLPoP/src/` ДО кодинга; диагноз платформы подтверждать артефактом (брейкпоинт/дамп/.asm), а не @@ -40,9 +49,11 @@ | # | Задача | Что | Блокирует | |---|--------|-----|-----------| | 1 | [GUARD-PHYS](#guard-phys) | физика стража = физика Кида (одна над `Char`) | **ядро сделано 2026-08-07**; открыт живой сценарий в MAME | -| 2 | [L3-CHOMP](#l3-chomp) | чомперы (5 шт) | прохождение ур. 3 | -| 3 | [L3-SKEL](#l3-skel) | скелет (единственный противник ур. 3) | прохождение ур. 3 | -| 4 | [L3-PASS](#l3-pass) | приёмка уровня 3 (обход комнат) | закрытие цели | +| — | [DRAW-CHAR](TASKS_CLOSED.md#draw-char) | отрисовка одна на всех `Char` — **закрыта и проверена 2026-08-08** | — | +| — | [MEM-BANK2](#mem-bank2) | разгрузка банка 2 — **сделана 2026-08-08**: 90.4 % → 72.9 %, свободно 4442 Б | — | +| 2 | [L3-CHOMP](#l3-chomp) | **СЛЕДУЮЩАЯ**: чомперы (5 шт) | прохождение ур. 3 | +| — | [L3-SKEL](#l3-skel) | скелет ур. 3 — **сделан 2026-08-07**, ждёт финальной приёмки | — | +| 3 | [L3-PASS](#l3-pass) | приёмка уровня 3 (обход комнат) | закрытие цели | | — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент | | — | [TUNE-1](#tune-1) | параметры движка → cfg-файл (сейчас `pop_tune.h`) | отладка таймингов и моды; берётся по мере надобности | @@ -173,6 +184,66 @@ скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ живой страж — переход не происходит. +### MEM-BANK2. Разгрузка банка 2 — **шаги 1-2 сделаны 2026-08-08** + +Банк 2 (`pop_bg.c`) упирался в потолок: 90.8 % ещё до чомперов. Разбор по +символам показал, что он держит ДВЕ разные вещи — горячий fore-проход (каждый +кадр) и холодную отрисовку тайлов (вход в комнату, точечные перерисовки), — +а общие «листья» (`blit_b`, `tile_code`, `tile_table`, `wall_pattern`) нужны +обеим. + +**Ключевое ограничение платформы** (записано в memory +`sdcc_banked_call_rules`): писучие данные банка лежат в `_DATA`/W2 и видны +всем, а `const`-таблицы — в СТРАНИЦЕ банка, и из другого банка их не +прочитать вовсе. Плюс трамплин выбирается ОБЪЯВЛЕНИЕМ: пометить лист +`__banked` — значит заставить платить и горячих вызывающих (654 такта на +круг). + +**Шаг 1 — общие листья в РЕЗИДЕНТ** (`pop_tile.c` + внутренний `pop_tile.h`). +W1 замаплен всегда, поэтому и горячая половина, и будущая холодная зовут их +прямым `call` и читают таблицы напрямую — без дублей и без трамплинов. +Уехали: `blit_b`, `env_b/wall_b/fore_b/pot_b`, `tile_code/tile_mod/ +tile_code_drawn/wall_modifier`, `heal_off`, `get_loose_frame`, +`potion_flask`, `pop_fore_set_clip`, `pop_room_set_below/above` + таблицы +`tile_table`, `COL_XH`, `WALL_FRAM_*`, `SPIKES_FRAM_LEFT`, +`LOOSE_FRAM_LEFT/BOTTOM`. Заодно три функции перестали быть `__banked` — +минус трамплин на кадр. + +**Шаг 2 — дедуп внутри банка.** `wall_pattern` (808 → 394) и `wall_rnd` +(786 → 654): четыре ветки по виду стены (SWS/SWW/WWS/WWW) отличались ТОЛЬКО +набором кусков и числами в одной и той же серии `prandom` — сведены к +таблицам `WP_PARTS` и `WR_RULE`. Порядок вызовов `prandom` сохранён +дословно (иначе разъедется раскладка кладки). + +**Грабля, пойманная замером:** после выноса `tile_table` из `static const` в +глобальную SDCC перестала сворачивать `tile_table[10].fore_x/fore_y` в +константы и стала читать их в рантайме — `potion_flask` раздулся 115 → 265 Б. +Лечится макросами, которыми инициализируется та же запись таблицы (одно место +правки, дублирования данных нет): 265 → 230. + +**Замер (ALLOCS=3000):** + +``` + было после ш.1 после ш.2 +BANK2 14 815 12 460 11 942 (90.4 % -> 72.9 %, свободно 4442 Б) +_CODE 20 064 22 591 22 556 (куча 6793 -> 4301 Б) +``` + +С релизным `ALLOCS=100000` банк 2 будет ещё примерно на 500-600 Б меньше +(замер до разгрузки: 14 815 -> 14 176). + +**Проверено:** `tests-host` зелёные; в MAME комната 1 уровня 1 после дедупа +совпала с дорефакторным снимком **попиксельно (0 различий из 227 520)**, +комната 3 (сплошная кладка, все четыре вида стены и марки) — та же раскладка +кирпича. + +**Что осталось в запасе, если места снова не хватит** (не делаем, пока нет +причины): вынести ХОЛОДНУЮ половину (`draw_tile` 1800 + точечные +перерисовки 1438 + падающие плиты 1898 ≈ 5.1 КБ) в свободный банк 7. Теперь +это дёшево: листья уже в резиденте, а `wall_pattern`/`wall_rnd`/марки/ +`draw_gate_back` нужны обеим половинам — им понадобится тонкая `__banked`- +обёртка (тело остаётся непомеченным, горячие вызывающие платят ноль). + ### L3-CHOMP. Чомперы — механика уровня 3 **Где:** 5 штук, комнаты 5(тайл 4), 16(23,24,25), 22(26). Дальше они почти @@ -183,10 +254,10 @@ коллизия и смерть Кида в сомкнутых челюстях (`seg004`/`seg006`); отрисовка кадра по модификатору (`draw_tile_anim`, ветка `tiles_18_chomper`). -**Риск:** банк 2 (`pop_bg`) занят на **90.8 %, свободно 1503 Б** (замер -2026-08-07, было 2213 Б) — считать -место ДО кодинга (`levels_plan.md` §5.1), иначе повторится «банк 2 упёрся -в потолок» (коммит 2f3e854). Свободные номера банков есть (5+). +**Место в банке 2 расчищено** ([MEM-BANK2](#mem-bank2), 2026-08-08): 72.9 %, +**свободно 4442 Б** (было 1074 Б). Считать место ДО кодинга всё равно +обязательно (`levels_plan.md` §5.1), и если не хватит — в запасе вынос +холодной половины `pop_bg` в банк 7, там расписано как. **Ассеты УЖЕ упакованы (2026-08-07):** `pop_pack_bg.py` кладёт в атлас весь набор кадров чомпера явно (`CHOMPER_BOT_IDS` 101-105, `CHOMPER_TOP_IDS` @@ -301,60 +372,36 @@ offset 22790, **240 байт** = 5 палитр × 16 цветов × 3 байт таблицу нельзя передавать прямо из банка (0xC000+), надо копировать в W1/W2 (см. [BUG-GUARD-COLOR-1](bug_closed.md#bug-guard-color-1)). -### DRAW-CHAR. Отрисовка — ОДНА на всех Char, как физика после GUARD-PHYS +### DRAW-COST. Стоимость fore-прохода после DRAW-CHAR -**Почему заведено (пользователь, 2026-08-07).** После GUARD-PHYS физика стала -общей над `Char`, а ОТРИСОВКА так и осталась продублированной: `kid_draw` и -`pop_guard_draw` считают `load_frame_to_obj`, флип и heal-прямоугольники -каждая по-своему, окклюзия — двумя проходами (`pop_fore_over_kid` / -`pop_fore_over_char`). Общее вынесено лишь частично (`char_footprint`, -`other_overlay_tile`, `pop_sword_draw`), а СПИСКИ ВЫЗОВОВ разные. +**Наблюдение пользователя на приёмке 2026-08-08:** циан-полоса профиля +(спрайты + fore) выросла — «в пределах, но оптимизация отдельной задачей». -**В оригинале она общая — проверено.** `seg008` держит два входа с -ИДЕНТИЧНЫМ телом: +**Отчего именно.** Раньше перебор тайлов у Кида шёл строго по футпринту +кадра (`char_x_left/right`, seg006:1021), а он УЖЕ спрайта; расширение +перебора окном спрайта (клинок и брызги уходят за габарит кадра) было +только у соперника. DRAW-CHAR сделала его общим — то есть у Кида теперь на +колонку-другую больше `fore_tile` за кадр. Это не регрессия «лишней +работы», а недостающая ранее корректность: тайл, который спрайт задевает, +обязан вернуть свой передний слой поверх него. -``` -add_kid_to_objtable (seg008:22F0) add_guard_to_objtable (seg008:2324) - loadkid() loadshad() - load_fram_det_col() load_fram_det_col() - load_frame_to_obj() load_frame_to_obj() - stuck_lower() stuck_lower() - set_char_collision() set_char_collision() - set_objtile_at_char() set_objtile_at_char() - redraw_at_char() redraw_at_char() - redraw_at_char2() redraw_at_char2() - clip_char() clip_char() - add_objtable(0) add_objtable(1 тень / 2 страж) -``` +**Куда копать, когда возьмёмся** (сначала МЕРИТЬ, потом резать — +[`pop_fore_layer_cost`](../../../docs) уже показывал 78 % кадра до кэша +кладки): +1. расширять перебор ТОЛЬКО по накладным спрайтам (клинок/брызги), а не по + всему окну: габарит самого кадра уже покрыт футпринтом + правилом меча; +2. кэш «в этом тайле fore-слоя нет вовсе» — половина колонок пустые, а за + каждую платим `tile_in_fclip` + разбор кладки; +3. считать окно клипа в тайловых координатах один раз, а не сравнивать + пиксельные габариты в двух циклах. -Различаются ТОЛЬКО окно (`Kid`/`Guard`), тип объекта и спецкейс тени у -зеркала (уровень 4). То есть та же схема, что `play_kid_frame` / -`play_guard_frame` у физики. +### DRAW-CHAR — **ЗАКРЫТА И ПРОВЕРЕНА 2026-08-08** -**Цена дублирования уже видна.** Скелет, падающий в пропасть (ур. 3), -рисовался ПОВЕРХ верхней грани пола соседней колонки: проход -`other_overlay_tile` (порядок midtable — «тайлы позже персонажа ложатся -поверх него») вызывался только у Кида. Симптом закрыт 2026-08-07 -переносом прохода в `pop_fore_over_char`, но корень — именно дублирование. -Раньше это не всплывало потому, что ОБЫЧНЫЙ страж в пропасть не падает: его -физика гейтится `x ∈ [44,211)`, а провалившегося убирает -`check_guard_fallout`. Тем же путём пойдут тень (ур. 4/5/6/12), визирь -(13), толстяк (12) — расхождение будет повторяться на каждом. - -**Что сделать:** свести к одному набору функций над `Char`, оставив -параметрами ровно то, что различается в оригинале: -- окно (`loadkid`/`loadshad` — уже есть); -- набор атласов и таблица кадров (`kid*` / `GUARD` / `SKEL` — уже - выбирается в `pop_guard_load`); -- слот heal-прямоугольников (у каждого персонажа свой, по страницам - дабл-буфера); -- тип объекта для порядка (Kid / тень / страж). - -**Не портированы вовсе и относятся сюда же:** `stuck_lower`, `redraw_at_char2`, -`clip_char` (см. memory `pop_clip_char_todo`). - -**Чем подтверждать:** падение скелета в пропасть с разных X (сегодняшний -сценарий), бой стража на ур. 1-2 без регрессий, `tests-host` зелёные. +Отрисовка сведена к одному набору функций над `Char` (`pop_cdraw.c`), проход +окклюзии — один на всех (`pop_fore_over_char`). Разбор, таблица «было → +стало», пять починенных расхождений и замеры — в +[`TASKS_CLOSED.md`](TASKS_CLOSED.md#draw-char). Оттуда же список того, что +надо потрогать вживую при приёмке уровней. ### L3-PASS. Приёмка уровня 3 — обход всех комнат diff --git a/applications/PoP/roomtest/_pop_draw.h b/applications/PoP/roomtest/_pop_draw.h index 712418b..1cbe465 100644 --- a/applications/PoP/roomtest/_pop_draw.h +++ b/applications/PoP/roomtest/_pop_draw.h @@ -10,7 +10,7 @@ * то есть из общего заголовка эти ~100 Б размножились бы десятком мёртвых * копий (замер CLIP-1, 2026-08-01: +1091 Б в _CODE и +636 Б в банке * стража — за код, который там никто не вызывает). Здесь их видят ровно - * три файла, которые реально рисуют: pop_bg.c, pop_kid.c, pop_gdraw.c. + * три файла, которые реально рисуют: pop_bg.c, pop_kid.c, pop_cdraw.c. */ #ifndef POP_DRAW_H #define POP_DRAW_H diff --git a/applications/PoP/roomtest/kid_data.h b/applications/PoP/roomtest/kid_data.h index 9899e4c..f7e2901 100644 --- a/applications/PoP/roomtest/kid_data.h +++ b/applications/PoP/roomtest/kid_data.h @@ -24,23 +24,23 @@ typedef struct { uint8_t image; int8_t dx, dy; uint8_t flags, sword; } kframe; #define KID_BIN_GFRAMES_OFF 3515 #define KID_NSEQ 115 -static const uint16_t kid_seq_off[KID_NSEQ] = { - 0x0000,0x1973,0x19A0,0x1A93,0x1ACD,0x1B39,0x1B5A,0x1BDB, - 0x1C54,0x1CA1,0x1D04,0x1D25,0x1D49,0x1D4F,0x1D68,0x1DF6, - 0x1C84,0x202B,0x1C1C,0x1C01,0x205A,0x1C38,0x209C,0x1D36, - 0x1C69,0x1CD1,0x1B04,0x1B1A,0x1D7D,0x1F9E,0x1F92,0x1F82, - 0x1F72,0x1F5D,0x1F48,0x1F33,0x1F19,0x1F13,0x1EF7,0x1EDA, - 0x1EBB,0x1E9C,0x1E7D,0x1B53,0x1E59,0x1E06,0x1E3B,0x1DFC, - 0x1D9B,0x1FB3,0x1FA7,0x20BE,0x20D1,0x20D4,0x20C9,0x19C4, - 0x1A13,0x1A2E,0x1A3C,0x1A7C,0x1A83,0x1A63,0x1A5E,0x1A68, - 0x1A6E,0x1A75,0x1A5A,0x19F9,0x1CEC,0x1A54,0x20DD,0x20AE, - 0x1E78,0x1CDC,0x19DC,0x1A42,0x1A07,0x19A8,0x2009,0x1B1B, - 0x1A8B,0x1B85,0x1BA1,0x1BBF,0x196E,0x20A9,0x1A22,0x19A6, - 0x19AC,0x1B2D,0x19C1,0x1FCA,0x1FDA,0x1FFB,0x2195,0x2132, - 0x214F,0x2166,0x2199,0x21A8,0x216D,0x226E,0x2136,0x21BC, - 0x1BFA,0x2245,0x2253,0x225B,0x21C4,0x21C0,0x21E6,0x21EA, - 0x21FC,0x2240,0x2257, -}; +#define KID_SEQ_OFF_INIT { \ + 0x0000,0x1973,0x19A0,0x1A93,0x1ACD,0x1B39,0x1B5A,0x1BDB, \ + 0x1C54,0x1CA1,0x1D04,0x1D25,0x1D49,0x1D4F,0x1D68,0x1DF6, \ + 0x1C84,0x202B,0x1C1C,0x1C01,0x205A,0x1C38,0x209C,0x1D36, \ + 0x1C69,0x1CD1,0x1B04,0x1B1A,0x1D7D,0x1F9E,0x1F92,0x1F82, \ + 0x1F72,0x1F5D,0x1F48,0x1F33,0x1F19,0x1F13,0x1EF7,0x1EDA, \ + 0x1EBB,0x1E9C,0x1E7D,0x1B53,0x1E59,0x1E06,0x1E3B,0x1DFC, \ + 0x1D9B,0x1FB3,0x1FA7,0x20BE,0x20D1,0x20D4,0x20C9,0x19C4, \ + 0x1A13,0x1A2E,0x1A3C,0x1A7C,0x1A83,0x1A63,0x1A5E,0x1A68, \ + 0x1A6E,0x1A75,0x1A5A,0x19F9,0x1CEC,0x1A54,0x20DD,0x20AE, \ + 0x1E78,0x1CDC,0x19DC,0x1A42,0x1A07,0x19A8,0x2009,0x1B1B, \ + 0x1A8B,0x1B85,0x1BA1,0x1BBF,0x196E,0x20A9,0x1A22,0x19A6, \ + 0x19AC,0x1B2D,0x19C1,0x1FCA,0x1FDA,0x1FFB,0x2195,0x2132, \ + 0x214F,0x2166,0x2199,0x21A8,0x216D,0x226E,0x2136,0x21BC, \ + 0x1BFA,0x2245,0x2253,0x225B,0x21C4,0x21C0,0x21E6,0x21EA, \ + 0x21FC,0x2240,0x2257, \ +} /* sword_tbl (seg006:2047): клинок отдельным спрайтом chtab_0. * Индекс — sword_frame (кадр.sword & 0x3F), id 255 = клинка нет. diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index 6f54494..bdd93b5 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -14,19 +14,15 @@ #include #include #include "pop_bg.h" +#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-порядок плиты) */ #include "pop_guard.h" /* SWORD_2_DRAWN — расширение футпринта под клинок */ -/* ---- Атласы (прямая адресация) ------------------------------------- */ -static atlas_t env[5]; -static atlas_t wall_at; -static atlas_t fore_at; -static atlas_t pot_at; /* chtab_1: зелья (склянка + пузырёк) */ - static const char *const ENV_ATL[5] = { /* Ресурсы разложены по каталогам образа (8.3, как в DSS): BG — фон, * KID — персонаж, GUARD — стражи, LEVELS — уровни. Иначе корень диска @@ -39,10 +35,10 @@ int pop_bg_load(void) __banked { int i; for (i = 0; i < 5; i++) - if (atlas_load(&env[i], ENV_ATL[i]) != 0) return -1; - if (atlas_load(&wall_at, "BG\\pop_wall.atl") != 0) return -1; - if (atlas_load(&fore_at, "BG\\pop_fore.atl") != 0) return -1; - if (atlas_load(&pot_at, "BG\\pop_pot.atl") != 0) return -1; + 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; } @@ -50,105 +46,16 @@ void pop_bg_free(void) __banked { int i; for (i = 0; i < 5; i++) - atlas_free(&env[i]); - atlas_free(&wall_at); - atlas_free(&fore_at); - atlas_free(&pot_at); -} - -/* Верхний клип блитов фона (ЭКРАННАЯ y): !=0 — строки кадра с screen_y < - * g_clip_top не рисуются (частичным блитом со сдвигом источника). Нужно - * полосе кладки у потолка (row<0): высокие кадры не должны лезть в верхний - * борт (за пределы игрового поля). 0 — без клипа. */ -static int g_clip_top; - -/* Блит ленты idx атласа a: НИЗ спрайта на ybottom, левый край x, - * прозрачно (0xFF). w/h читаются из getimage-заголовка ленты (в W0). - * При g_clip_top рисуем только часть кадра ниже границы (gfx_blit_part). */ -/* ОКНО FORE-СЛОЯ (экранные координаты, полуинтервал). Пока оно включено, - * blit_b рисует только ту часть куска, что попадает в окно. - * - * Зачем: fore-слой существует РОВНО для того, чтобы вернуть куски тайлов - * поверх нарисованного персонажа — за пределами его прямоугольника в - * видеопамяти и так лежит правильный фон. А рисовал он тайлы ЦЕЛИКОМ: - * стена (код 20) — блок 64×63 = 4032 пикселя, и таких кусков за проход - * до девяти. Замер в MAME 2026-07-29 (комната 3, Kid + страж): проход - * поверх Kid — 402 816 тактов, поверх стража — 431 964, вместе 78 % всей - * работы кадра при бюджете 430 000 тактов на кадр. С окном большинство - * кусков отсекается целиком, остальные режутся до пересечения. */ -static int fclip_x0, fclip_y0, fclip_x1, fclip_y1; -static uint8_t fclip_on; - -void pop_fore_set_clip(int x, int y, int w, int h) __banked -{ - fclip_x0 = x; fclip_y0 = y; - fclip_x1 = x + w; fclip_y1 = y + h; -} - -static void blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) -{ - const uint8_t *img; - uint16_t w, h; - int top, dx, dy, dw, dh, sx = 0, sy = 0; - if (idx >= a->count) - return; - /* Грубый отсев ДО маппинга страницы: реальные w/h лежат в заголовке - * ленты, то есть в EMM-странице, и чтобы их прочитать, надо сделать - * gfx_w0_map. Берём заведомо больший габарит (кадры фона не крупнее - * 64×64) и, если он не задевает окно, уходим не заплатив ни за - * atlas_image, ни за map/unmap. */ - if (fclip_on && - (x >= fclip_x1 || x + 64 <= fclip_x0 || - ybottom + POP_YOFF + 1 <= fclip_y0 || - ybottom + POP_YOFF - 64 >= fclip_y1)) - return; - img = (const uint8_t *)atlas_image(a, idx); - gfx_w0_map(a->page); - w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); - h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); - top = ybottom - (int)h + 1 + POP_YOFF; /* +YOFF: центрирование */ - dx = x; dy = top; dw = (int)w; dh = (int)h; - if (g_clip_top && dy < g_clip_top) { /* полоса у потолка */ - sy = g_clip_top - dy; dy = g_clip_top; dh -= sy; - } - if (fclip_on) { /* окно fore-слоя */ - if (dx < fclip_x0) { int d = fclip_x0 - dx; sx += d; dx += d; dw -= d; } - if (dy < fclip_y0) { int d = fclip_y0 - dy; sy += d; dy += d; dh -= d; } - if (dx + dw > fclip_x1) dw = fclip_x1 - dx; - if (dy + dh > fclip_y1) dh = fclip_y1 - dy; - } - if (w && h && dw > 0 && dh > 0) { - /* Целиком на экране — линейное ядро без клипа: ~2.9× быстрее - * (замер: 4617 против 13288 тактов на спрайт 32×3; общее ядро - * gfx_blit платит за клип/16-бит/split, а не за пиксели). - * W3-скобку ставит сама libbgi: из кода, собранного с --w3, её - * вызывать нельзя (окно W3 уходит под видеобанк). */ - if (dx >= 0 && dy >= 0 && dw < 256 && dh < 256 && - dx + dw <= 320 && dy + dh <= 256 && w < 256) { - if (sx == 0 && sy == 0 && dw == (int)w && dh == (int)h) - gfx_blit_noclip(dx, dy, img); - else - gfx_blit_part_noclip(dx, dy, img, (uint8_t)sx, (uint8_t)sy, - (uint8_t)dw, (uint8_t)dh); - } else { - gfx_blit_part(dx, dy, img, sx, sy, dw, dh); /* край экрана — с клипом */ - } - } - gfx_w0_unmap(); -} - -/* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. - * Выбор ядра (noclip / клипающее) — общий с Кидом и стражем, pop_heal_fast - * в pop_bg.h. */ -static void heal_off(int x, int y, int w, int h) -{ - pop_heal_fast(x, y + POP_YOFF, w, h); + 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/полоса потолка). Оверлей включается * только в позах виса/полёта/подъёма и рисует НЕ по прямоугольнику Kid, - * поэтому kid_heal его следы не убирает: на второй странице дабл-буфера + * поэтому heal персонажа его следы не убирает: на второй странице дабл-буфера * остаётся кадр «с оверлеем», и он мерцает через кадр. Копим bbox и * восстанавливаем фон на ОБЕИХ страницах (pop_fore_heal). */ static int ov_x0, ov_y0, ov_x1, ov_y1; @@ -170,67 +77,15 @@ void pop_fore_heal(void) __banked { uint8_t pg = gfx_get_draw_page() & 1; if (!(ov_pending & (1 << pg))) return; - heal_off(ov_x0, ov_y0, ov_x1 - ov_x0, ov_y1 - ov_y0); + pop_heal_off(ov_x0, ov_y0, ov_x1 - ov_x0, ov_y1 - ov_y0); ov_pending = (uint8_t)(ov_pending & ~(1 << pg)); } -static void env_b(uint8_t id, int x, int yb) { blit_b(&env[id >> 5], (uint8_t)(id & 31), x, yb); } -static void wall_b(uint8_t id, int x, int yb) { blit_b(&wall_at, id, x, yb); } -static void fore_b(uint8_t id, int x, int yb) { blit_b(&fore_at, id, x, yb); } -static void pot_b(uint8_t id, int x, int yb) { blit_b(&pot_at, id, x, yb); } - - - -/* ---- Таблица тайлов (piece, seg008.c:27) --------------------------- */ -typedef struct { - uint8_t base_id, floor_left; int8_t base_y; - uint8_t right_id, floor_right; int8_t right_y; - uint8_t stripe_id, topright_id, bottom_id, fore_id, fore_x; int8_t fore_y; -} piece; - -static const piece tile_table[31] = { - { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, /* 00 empty */ - { 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 01 floor */ - {127, 1, 0, 133, 1, 2, 145, 0, 43, 0, 0, 0}, /* 02 spike */ - { 92, 1, 0, 93, 1, 2, 0, 94, 43, 95, 1, 0}, /* 03 pillar */ - { 46, 1, 0, 47, 1, 2, 0, 48, 43, 49, 3, 0}, /* 04 gate */ - { 41, 1, 1, 35, 1, 3, 145, 0, 36, 0, 0, 0}, /* 05 stuck floor */ - { 41, 1, 0, 42, 1, 2, 145, 0, 96, 0, 0, 0}, /* 06 close button */ - { 46, 1, 0, 0, 0, 2, 0, 0, 43, 49, 3, 0}, /* 07 door top w/floor */ - { 86, 1, 0, 87, 1, 2, 0, 0, 43, 88, 1, 0}, /* 08 big pillar bottom */ - { 0, 0, 0, 89, 0, 3, 0, 90, 0, 91, 1, 3}, /* 09 big pillar top */ - { 41, 1, 0, 42, 1, 2, 145, 0, 43, 12, 2, -3}, /* 0A potion */ - { 0, 1, 0, 0, 0, 0, 145, 0, 0, 0, 0, 0}, /* 0B loose floor */ - { 0, 0, 0, 0, 0, 2, 0, 0, 85, 49, 3, 0}, /* 0C door top */ - { 75, 1, 0, 42, 1, 2, 0, 0, 43, 77, 0, 0}, /* 0D mirror */ - { 97, 1, 0, 98, 1, 2, 145, 0, 43,100, 0, 0}, /* 0E debris */ - {147, 1, 0, 42, 1, 1, 145, 0,149, 0, 0, 0}, /* 0F open button */ - { 41, 1, 0, 37, 0, 0, 0, 38, 43, 0, 0, 0}, /* 10 leveldoor left */ - { 0, 0, 0, 39, 1, 2, 0, 40, 43, 0, 0, 0}, /* 11 leveldoor right */ - { 0, 0, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 12 chomper */ - { 41, 1, 0, 42, 1, 2, 0, 0, 43, 0, 0, 0}, /* 13 torch */ - { 0, 0, 0, 1, 1, 2, 0, 2, 0, 0, 0, 0}, /* 14 wall */ - { 30, 1, 0, 31, 1, 2, 0, 0, 43, 0, 0, 0}, /* 15 skeleton */ - { 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 16 sword */ - { 41, 1, 0, 10, 0, 0, 0, 11, 43, 0, 0, 0}, /* 17 balcony left */ - { 0, 0, 0, 12, 1, 2, 0, 13, 43, 0, 0, 0}, /* 18 balcony right */ - { 92, 1, 0, 42, 1, 2, 145, 0, 43, 95, 1, 0}, /* 19 lattice pillar */ - { 1, 0, 0, 0, 0, 0, 0, 0, 2, 9, 0,-53}, /* 1A lattice down */ - { 3, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1B lattice small */ - { 4, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1C lattice left */ - { 5, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1D lattice right */ - { 97, 1, 0, 98, 1, 2, 0, 0, 43,100, 0, 0}, /* 1E debris w/torch */ -}; - /* ---- Константные таблицы (render_room.py) -------------------------- */ -static const uint8_t COL_XH[10] = {0, 4, 8, 12, 16, 20, 24, 28, 32, 36}; static const uint8_t TBL_LINE[3] = {0, 10, 20}; -static const uint8_t WALL_FRAM_BOTTOM[4] = {7, 9, 5, 3}; -static const uint8_t WALL_FRAM_MAIN[4] = {8, 10, 6, 4}; 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_LEFT[10] = {0, 128, 129, 130, 131, 132, 131, 129, 128, 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}; @@ -259,120 +114,41 @@ 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_flask(int row, int col, uint8_t modif) /* draw_tile_fore */ -{ - uint8_t type = potion_type(modif); - uint8_t id = 12; - if (type == 0) return; - if (type >= 2 && type < 5) id = 13; /* большая склянка */ - pot_b(id, (tile_table[10].fore_x + COL_XH[col]) * 8 + 6, - tile_table[10].fore_y + 63 * row + 62); -} - 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 = (COL_XH[col] + 3) * 8 + 1; + int x = (POP_COL_XH[col] + 3) * 8 + 1; int yb = 63 * row + 62 - (pot_size << 2) - 14; if (type == 0) return; - if (fore) pot_b(POTION_FRAM_BUBB[modif & 7], x, yb); /* кадр пузырька */ - else pot_b(POT_MASK_ID, x, yb); /* маска (под ним) */ + if (fore) pop_pot_b(POTION_FRAM_BUBB[modif & 7], x, yb); /* кадр пузырька */ + else pop_pot_b(POT_MASK_ID, x, yb); /* маска (под ним) */ } -/* ---- Текущая комната (globals для tile_code/mod) ------------------- */ -static const uint8_t *g_fg, *g_bg, *g_lfg, *g_lbg; -static const uint8_t *g_below; /* row0-коды комнаты СНИЗУ (для topright под нижним рядом) */ -static const uint8_t *g_above, *g_abovemod; /* row2 fg/mod комнаты СВЕРХУ (полоса кладки у потолка) */ -static uint8_t g_room; - -/* Задать ряд 0 комнаты снизу (fg-коды) — нужен для draw_tile_topright/ - * floorright, когда тайл нижнего ряда стал видимым (empty после падения - * loose): «стена вниз» = топрайт тайла room_B(0, col-1). 0 — нет комнаты - * снизу (кромка уровня = стена). */ -void pop_room_set_below(const uint8_t *below_row0_fg) __banked { g_below = below_row0_fg; } - -/* Задать ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки - * у потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom - * при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */ -void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked -{ g_above = above_row2_fg; g_abovemod = above_row2_mod; } - -static uint8_t tile_code(int row, int col) -{ - if (col < 0) { - if (g_lfg && row >= 0 && row < 3) return g_lfg[row]; - return 20; /* уровневая кромка = стена */ - } - if (row < 0) { /* верхняя полоса: ряд 2 комнаты сверху */ - if (row == -1 && g_above && col < 10) return g_above[col]; - return 1; /* нет комнаты сверху → пол (кромка) */ - } - if (row > 2 || col > 9) return 20; - return g_fg[row * 10 + col]; -} - -static uint8_t tile_mod(int row, int col) -{ - if (col < 0) { - if (g_lbg && row >= 0 && row < 3) return g_lbg[row]; - return 0; - } - if (row == -1 && g_abovemod && col < 10) return g_abovemod[col]; - if (row < 0 || row > 2 || col > 9) return 0; - return g_bg[row * 10 + col]; -} - -/* Тип тайла «как он РИСУЕТСЯ» — порт подстановки нажатой кнопки из - * get_tile_to_draw (seg008:240): opener → floor, closer → stuck, когда таймер - * связи > 1. Любой слой, который выбирает ветку отрисовки по типу тайла, - * обязан смотреть на ПОДСТАВЛЕННЫЙ тип (иначе кнопка, на которой стоит Kid, - * ведёт себя как «не пол» — напр. climb-оверлей уходил в draw_other_overlay - * и закрашивал Kid целым тайлом вместо узкой кромки floor_left_overlay). */ -static uint8_t tile_code_drawn(int row, int col) -{ - uint8_t code = tile_code(row, col); - if (code == 0x0F) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 1; } - else if (code == 0x06) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 5; } - return code; -} - -/* seg008.c:1255 (без FAKE_TILES): сосед — стена, если его тип == 20. - * На кромке col0 сосед по умолчанию = стена (как render_room). */ -static uint8_t wall_modifier(int row, int col) -{ - uint8_t lw = (col > 0) ? (tile_code(row, col - 1) == 20) : 1; - uint8_t rw = (col < 9) ? (tile_code(row, col + 1) == 20) : 1; - if (lw && rw) return 3; /* WWW */ - if (lw) return 2; /* WWS */ - if (rw) return 1; /* SWW */ - return 0; /* SWS */ -} - -/* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- */ -static pop_rnd_t rnd_seed; - -#define prandom(maxv) pop_prandom(&rnd_seed, (maxv)) +/* ---- PRNG (seg009.c:321, 32-бит LCG) — только на ВХОД в комнату ---- * + * Сид (pop_t_seed) живёт в резиденте: его ставит полная отрисовка комнаты, а + * читает кэш раскладки кладки. */ +#define prandom(maxv) pop_prandom(&pop_t_seed, (maxv)) /* ---- Декали-марки стен (seg008.c:2041/2056) ------------------------ */ static void draw_left_mark(uint16_t dv, int arg2, int arg1, int row, int col) { uint8_t image_id = (dv & 1) ? RES_WALL_MARK_BL : RES_WALL_MARK_TL; int lv2 = 0; - int xh = COL_XH[col]; + int xh = POP_COL_XH[col]; int dby = 63 * row + 65; if (dv > 3) lv2 = arg1 + 6; else if (dv > 1) lv2 = arg2 + 6; - wall_b(image_id, (xh + ((dv == 2 || dv == 3) ? 1 : 0)) * 8 + lv2, dby - LPOS[dv]); + pop_wall_b(image_id, (xh + ((dv == 2 || dv == 3) ? 1 : 0)) * 8 + lv2, dby - LPOS[dv]); } static void draw_right_mark(uint16_t dv, int arg1, int row, int col) { uint8_t image_id = (dv & 1) ? RES_WALL_MARK_BR : RES_WALL_MARK_TR; int a = (dv < 2) ? 24 : arg1 - 3; - int xh = COL_XH[col]; + int xh = POP_COL_XH[col]; int dby = 63 * row + 65; - wall_b(image_id, (xh + ((dv > 1) ? 1 : 0)) * 8 + a, dby - RPOS[dv]); + pop_wall_b(image_id, (xh + ((dv > 1) ? 1 : 0)) * 8 + a, dby - RPOS[dv]); } /* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */ @@ -407,6 +183,20 @@ static void wall_pattern_reset(void) /* смена комнаты — сид static uint8_t wr_mid_div, wr_mid_off, wr_bot_div, wr_bot_off; static uint8_t wr_rndblk, wr_right, wr_rightv, wr_left, wr_leftv; +/* Правила серии prandom по модификатору стены (bg): что вообще есть у этого + * вида кладки и с какими порогами. Столбцы: + * [0] есть «случайный блок» (тогда prandom(4)==0) + * [1] порог условия метки СПРАВА (0 = метки справа нет, prandom не зовётся) + * [2] порог значения метки справа, [3] прибавка к нему + * [4] порог условия метки СЛЕВА (есть у всех четырёх), [5] порог значения + * Порядок строк — по bg: 0 SWS, 1 SWW, 2 WWS, 3 WWW (seg008:1928). */ +static const uint8_t WR_RULE[4][6] = { + {0, 0, 0, 0, 6, 1}, /* SWS: только метка слева */ + {1, 4, 3, 0, 4, 3}, /* SWW */ + {0, 4, 1, 2, 4, 4}, /* WWS: значение справа со сдвигом 2 */ + {1, 4, 3, 0, 4, 4}, /* WWW: полный набор */ +}; + static void wall_rnd(int row, int col) { /* Кэшируем только штатную сетку: колонка вне 0..9 дала бы индекс @@ -423,10 +213,10 @@ static void wall_rnd(int row, int col) wr_left = (uint8_t)((b >> 4) & 1); wr_leftv = (uint8_t)((b >> 5) & 7); return; } - saved = rnd_seed; + saved = pop_t_seed; /* Верхняя полоса (row<0): SDLPoP рисует её как ряд 2 комнаты сверху * (drawn_row=2 для seed, draw_main_y=-1 для позиции) — seed по row 2. */ - pop_prandom_set(rnd_seed, g_room + TBL_LINE[row < 0 ? 2 : row] + col); + pop_prandom_set(pop_t_seed, pop_t_room + TBL_LINE[row < 0 ? 2 : row] + col); prandom(1); /* сброс */ wr_mid_div = (uint8_t)prandom(1); wr_mid_off = (uint8_t)prandom(4); @@ -434,22 +224,22 @@ static void wall_rnd(int row, int col) wr_bot_off = (uint8_t)prandom(4); wr_rndblk = wr_right = wr_left = 0; wr_rightv = wr_leftv = 0; - bg = wall_modifier(row, col); - if (bg == 3) { /* WWW */ - wr_rndblk = (uint8_t)(prandom(4) == 0); - if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)prandom(3); } - if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(4); } - } else if (bg == 0) { /* SWS */ - if (prandom(6) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(1); } - } else if (bg == 1) { /* SWW */ - wr_rndblk = (uint8_t)(prandom(4) == 0); - if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)prandom(3); } - if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(3); } - } else { /* WWS (bg==2) */ - if (prandom(4) == 0) { wr_right = 1; wr_rightv = (uint8_t)(prandom(1) + 2); } - if (prandom(4) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(4); } + bg = pop_wall_modifier(row, col); + { /* Четыре ветки bg отличались ТОЛЬКО числами в одной и той же серии + * prandom, поэтому они сведены к правилу (WR_RULE). ПОРЯДОК вызовов + * prandom обязан совпадать с оригиналом дословно, включая то, что + * значение метки берётся ТОЛЬКО когда сработало её условие: иначе + * последовательность разъедется и кладка станет другой. «Нет метки + * справа» = нулевой порог условия — тогда prandom не зовётся вовсе, + * ровно как в ветке SWS. */ + const uint8_t *r = WR_RULE[bg]; + if (r[0]) wr_rndblk = (uint8_t)(prandom(4) == 0); + if (r[1] && prandom(r[1]) == 0) { + wr_right = 1; wr_rightv = (uint8_t)(prandom(r[2]) + r[3]); + } + if (prandom(r[4]) == 0) { wr_left = 1; wr_leftv = (uint8_t)prandom(r[5]); } } - rnd_seed = saved; + pop_t_seed = saved; if (idx >= 0 && idx < WP_N) { wp_a[idx] = (uint8_t)(wr_mid_div | (wr_mid_off << 1) | (wr_bot_div << 4) | (wr_bot_off << 5)); @@ -459,50 +249,50 @@ static void wall_rnd(int row, int col) } } -/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- */ +/* ---- wall_pattern (seg008.c:1928, dungeon-ветка) ------------------- * + * Куски кладки одни и те же для всех четырёх видов стены, различается ровно + * НАБОР присутствующих (WP_PARTS) — раньше это были четыре ветки с + * повторяющимся телом. Порядок кусков внутри вызова сохранён: случайный + * блок, средний делитель, нижний делитель, метка справа, метка слева. + * + * which_part == 0 — только нижний делитель (слой draw_tile_bottom), 1 — всё + * остальное (draw_tile_fore). У SWW нижнего делителя нет вовсе, у SWS — + * ничего, кроме метки слева. */ +#define WP_RND 1 +#define WP_MID 2 +#define WP_BOT 4 +#define WP_RIGHT 8 +#define WP_LEFT 16 + +static const uint8_t WP_PARTS[4] = { + WP_LEFT, /* 0 SWS */ + WP_RND | WP_MID | WP_RIGHT | WP_LEFT, /* 1 SWW */ + WP_MID | WP_BOT | WP_RIGHT | WP_LEFT, /* 2 WWS */ + WP_RND | WP_MID | WP_BOT | WP_RIGHT | WP_LEFT, /* 3 WWW */ +}; + static void wall_pattern(int row, int col, int which_part) { - int xh = COL_XH[col]; + int xh = POP_COL_XH[col]; int dby = 63 * row + 65; - uint8_t bg = wall_modifier(row, col); + uint8_t parts = WP_PARTS[pop_wall_modifier(row, col)]; wall_rnd(row, col); - if (bg == 3) { /* WWW */ - if (which_part) { - if (wr_rndblk) - wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42); - wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21); - } - wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby); - if (which_part) { - if (wr_right) - draw_right_mark(wr_rightv, wr_mid_off, row, col); - if (wr_left) - draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col); - } - } else if (bg == 0) { /* SWS */ - if (which_part && wr_left) - draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col); - } else if (bg == 1) { /* SWW */ - if (which_part) { - if (wr_rndblk) - wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42); - wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21); - if (wr_right) - draw_right_mark(wr_rightv, wr_mid_off, row, col); - if (wr_left) - draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col); - } - } else { /* WWS (bg==2) */ - if (which_part) - wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), (xh + 1) * 8 + wr_mid_off, dby - 21); - wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby); - if (which_part) { - if (wr_right) - draw_right_mark(wr_rightv, wr_mid_off, row, col); - if (wr_left) - draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, wr_bot_off - wr_bot_div, row, col); - } + if (which_part) { + if ((parts & WP_RND) && wr_rndblk) + pop_wall_b(RES_WALL_RNDBLOCK, xh * 8, dby - 42); + if (parts & WP_MID) + pop_wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_mid_div), + (xh + 1) * 8 + wr_mid_off, dby - 21); + } + if (parts & WP_BOT) + pop_wall_b((uint8_t)(RES_WALL_DIVIDER1 + wr_bot_div), xh * 8 + wr_bot_off, dby); + if (which_part) { + if ((parts & WP_RIGHT) && wr_right) + draw_right_mark(wr_rightv, wr_mid_off, row, col); + if ((parts & WP_LEFT) && wr_left) + draw_left_mark(wr_leftv, wr_mid_off - wr_mid_div, + wr_bot_off - wr_bot_div, row, col); } } @@ -510,9 +300,9 @@ static void wall_pattern(int row, int col, int which_part) static void draw_topright(uint8_t tt, int x, int dby) { if (tt == 7 || tt == 12) return; - if (tt == 20) { wall_b(2, x, dby); return; } - if (tile_table[tt].topright_id) - env_b(tile_table[tt].topright_id, x, dby); + 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). */ @@ -525,19 +315,19 @@ static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy) int yb, gate_frame; if (gate_bot_y + 12 < dmy) - env_b(50, x, gate_bot_y); + pop_env_b(50, x, gate_bot_y); else - env_b(51, x, gate_bot_y - 2); + pop_env_b(51, x, gate_bot_y - 2); yb = gate_bot_y - 12; if (yb < 192) { while (yb >= 0 && yb > 7 && (yb - 7) > gate_top_y) { - env_b(52, x, yb); + pop_env_b(52, x, yb); yb -= 8; } } gate_frame = yb - gate_top_y + 1; if (gate_frame > 0 && gate_frame < 9) - env_b(DOOR_FRAM_SLICE[gate_frame], x, yb); + pop_env_b(DOOR_FRAM_SLICE[gate_frame], x, yb); } /* ---- Loose floors: кадры дрожащей плиты (seg008 loose_fram_*) ------- * @@ -548,9 +338,7 @@ static void draw_gate_back(uint8_t modl, int xh, int dby, int dmy) extern uint8_t pop_loose_modif[30]; extern uint8_t pop_ceil_modif[10]; /* плита-потолок: loose ряда 2 комнаты сверху */ -static const uint8_t LOOSE_FRAM_LEFT[12] = {41,69,41,70,70,41,41,41,70,70,70,0}; static const uint8_t LOOSE_FRAM_RIGHT[12] = {42,71,42,72,72,42,42,42,72,72,72,0}; -static const uint8_t LOOSE_FRAM_BOTTOM[12] = {43,73,43,74,74,43,43,43,74,74,74,0}; /* Режим ЗАПЕЧКИ статического фона (запись идёт и в ОЗУ-копию акселератора, * из которой потом восстанавливает heal). В этом режиме дрожащие кадры @@ -561,16 +349,6 @@ static const uint8_t LOOSE_FRAM_BOTTOM[12] = {43,73,43,74,74,43,43,43,74,74,74,0 * (0,5), который рисуется в ячейке (0,6), был в этот момент в тряске. * Фон обязан быть состоянием ПОКОЯ — анимация каждый кадр кладётся поверх * (банк SPRITE, только видео-ОЗУ). */ -static uint8_t bake_rest; - -/* get_loose_frame (seg008:761): индекс дрожащего кадра по модификатору. */ -static uint8_t get_loose_frame(uint8_t m) -{ - if (bake_rest) return 0; /* запекаем фон → кадр покоя */ - if (m & 0x80) { m &= 0x7F; if (m > 10) return 1; } - return m; -} - /* draw_leveldoor (seg008:1D29) — дверь уровня. Рисуется при обработке * ПРАВОЙ половины (tile_left = 0x10), все куски со сдвигом +8px (xh+1): * 99 низ лестницы (всегда), @@ -591,40 +369,40 @@ static void draw_leveldoor(uint8_t modif, int xh, int dmy) * draw_tile_anim_right, разбирая левого соседа). +8 у palace-уровней * (tbl_level_type) — на уровне 1 не применяется. */ pop_leveldoor_right = xh * 8 + 48; - env_b(99, x, ybottom); + pop_env_b(99, x, ybottom); if (modif) - env_b(144, x, ybottom - 4); /* лестница (комната ≠ стартовой) */ + pop_env_b(144, x, ybottom - 4); /* лестница (комната ≠ стартовой) */ for (;;) { - env_b(33, x, ylo); + pop_env_b(33, x, ylo); if (y > ylo) ylo += 4; else break; } /* ИМЕННО после цикла: в оригинале clip_char читает уже доведённое * значение (низ поднятой створки), а не стартовое. */ pop_leveldoor_ybottom = ylo; - env_b(34, x, dmy - 64); + pop_env_b(34, x, dmy - 64); } /* ---- draw_tile (seg008.c:155) — одна клетка ------------------------ */ static void draw_tile(int row, int col) { - uint8_t code = tile_code(row, col); - uint8_t lcode = tile_code(row, col - 1); - uint8_t lmod = tile_mod(row, col - 1); + 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(tile_mod(row, col)) & 0x1F) > 1) code = 1; } - else if (code == 0x06) { if ((pop_doorlink2(tile_mod(row, col)) & 0x1F) > 1) code = 5; } + 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 = COL_XH[col]; + int xh = POP_COL_XH[col]; int x = xh * 8; int dby = 63 * row + 65; int dmy = dby - 3; - const piece *t = &tile_table[code]; - const piece *lt = &tile_table[lcode]; + const piece *t = &pop_tile_table[code]; + const piece *lt = &pop_tile_table[lcode]; uint8_t rbl_code; uint8_t wmod; @@ -637,9 +415,9 @@ static void draw_tile(int row, int col) * (room_BL). Её тайл (0,9) приходит одиннадцатым байтом below-ряда; * «стена» там стоит, только если такой комнаты нет — как в * load_rowbelow (seg008:368). См. BUG-SEAM-WEDGE-1. */ - if (lc < 0) rbl_code = g_below ? (uint8_t)(g_below[10] & 0x1F) : 20; - else rbl_code = g_below ? (uint8_t)(g_below[lc] & 0x1F) : 1; - } else rbl_code = tile_code(row + 1, col - 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) { @@ -652,22 +430,22 @@ static void draw_tile(int row, int col) /* --- draw_tile_right (seg008.c:456) --- */ if (code != 20) { if (lcode == 20) { - wall_b(1, x, tile_table[20].right_y + dmy); /* грань стены */ + pop_wall_b(1, x, pop_tile_table[20].right_y + dmy); /* грань стены */ } else if (lcode == 0) { if (lmod <= 3 && BLUELINE_FRAM1[lmod]) - env_b(BLUELINE_FRAM1[lmod], x, BLUELINE_FRAM_Y[lmod] + dmy); + pop_env_b(BLUELINE_FRAM1[lmod], x, BLUELINE_FRAM_Y[lmod] + dmy); } else if (lcode == 1) { uint8_t num; - env_b(42, x, tile_table[1].right_y + dmy); /* правый треуг. пола */ + pop_env_b(42, x, pop_tile_table[1].right_y + dmy); /* правый треуг. пола */ num = (lmod <= 3) ? lmod : 0; if (num != 0) /* dungeon: level_type=0 */ - env_b(BLUELINE_FRAM3[num], x, dmy - 20); /* силуэт кладки */ + pop_env_b(BLUELINE_FRAM3[num], x, dmy - 20); /* силуэт кладки */ } else { if (lt->right_id) - env_b(lt->right_id, x, lt->right_y + dmy); + pop_env_b(lt->right_id, x, lt->right_y + dmy); /* Основание факела (seg008:489): рисует ПРАВЫЙ сосед. */ if (lcode == 19 || lcode == 30) - env_b(146, x, dby - 28); + pop_env_b(146, x, dby - 28); /* stripe — только палас, dungeon пропускает */ } } @@ -679,11 +457,11 @@ static void draw_tile(int row, int col) if (lcode == 4) { /* gate: решётка */ draw_gate_back(lmod, xh, dby, dmy); } else if (lcode == 11) { /* loose: правая грань (анимир. по modif соседа-loose) */ - env_b(LOOSE_FRAM_RIGHT[get_loose_frame(pop_loose_modif[row * 10 + (col - 1)])], x, dby - 1); + 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]) - env_b(SPIKES_FRAM_RIGHT[sf], x, dmy - 7); + pop_env_b(SPIKES_FRAM_RIGHT[sf], x, dmy - 7); } else if (lcode == 16) { /* дверь уровня */ draw_leveldoor(lmod, xh, dmy); } @@ -694,18 +472,18 @@ static void draw_tile(int row, int col) /* --- draw_tile_bottom (seg008.c:570) --- */ if (code == 20) { - wmod = wall_modifier(row, col); - wall_b(WALL_FRAM_BOTTOM[wmod], x, dby); /* сплошная нижняя грань */ + 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) { - env_b(t->bottom_id, x, dby); + 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) - env_b(LOOSE_FRAM_BOTTOM[get_loose_frame(row >= 0 ? pop_loose_modif[row * 10 + col] + 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) базу НЕ @@ -713,11 +491,11 @@ static void draw_tile(int row, int col) if (row >= 0) { uint8_t base_id = t->base_id; if (code == 11) /* loose: левая грань (анимир.) */ - base_id = LOOSE_FRAM_LEFT[get_loose_frame(pop_loose_modif[row * 10 + col])]; + 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) - env_b(base_id, x, t->base_y + dmy); + pop_env_b(base_id, x, t->base_y + dmy); } /* --- draw_tile_anim (seg008.c:0B2B) — левый кадр выдвинутых пик в @@ -725,29 +503,29 @@ static void draw_tile(int row, int col) * рисуется только правым соседом со сдвигом → шахта над самой пикой * пустая. */ if (row >= 0 && code == 2) { - uint8_t sm = tile_mod(row, col); + uint8_t sm = pop_tile_mod(row, col); uint8_t sf = (sm & 0x80) ? 5 : sm; - if (sf > 0 && sf < 10 && SPIKES_FRAM_LEFT[sf]) - env_b(SPIKES_FRAM_LEFT[sf], x, dmy - 2); + 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) - pot_b((uint8_t)((tile_mod(row, col) == 1) + 10), x, dmy - 3); + 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 = wall_modifier(row, col); - wall_b(WALL_FRAM_MAIN[wmod], x, dmy); /* основная грань */ + 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). */ - potion_flask(row, col, tile_mod(row, col)); + pop_potion_flask(row, col, pop_tile_mod(row, col)); } else if (t->fore_id) { - fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); + pop_fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); } } @@ -757,14 +535,14 @@ static void draw_tile(int row, int col) * внутри draw_tile). */ void pop_loose_shake_draw(int row, int col) __banked { - int x = COL_XH[col] * 8; + int x = POP_COL_XH[col] * 8; /* heal возвращает печёный фон обоих тайлов из ОЗУ-копии, поверх — * перерисовка обоих тайлов с дрожащим кадром (draw_tile берёт кадр из * pop_loose_modif). Порт redraw_tile_height (loose + правый сосед). * NB: печёный кадр-0 может слегка просвечивать по кромкам — чистое * решение (bake тайла как empty + рисовать loose всегда динамически) * отложено; wipe-в-чёрный делал плиту невидимой — не годится. */ - heal_off(x, 63 * row + 46, 64, 24); + pop_heal_off(x, 63 * row + 46, 64, 24); gfx_set_bank(GFX_BANK_SPRITE); draw_tile(row, col); /* loose: лево+низ (анимир.) */ if (col + 1 < 10) @@ -774,12 +552,12 @@ void pop_loose_shake_draw(int row, int col) __banked /* Перерисовать пики тайла (row,col) на back-странице по ЖИВОМУ modif * (pop_trob). Кадр выдвижения рисует ПРАВЫЙ сосед (draw_tile ветка lcode==2 - * читает lmod = g_bg[row*10+col] = живой modif пики). g_bg должен указывать + * читает 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 = COL_XH[col] * 8; + int x = POP_COL_XH[col] * 8; /* Порт redraw_21h -> redraw_tile_height (seg007:0218): wipe + ПОЛНЫЙ * redraw ДВУХ тайлов — своего и правого соседа (остриё рисуется в его * ячейке). Именно ПОЛНЫЙ: рисовать только два спрайта остриёв нельзя — @@ -789,7 +567,7 @@ void pop_spike_redraw(int row, int col) __banked * в ячейке col — правое остриё пики col-1 (ветка lcode==2). * Шипы выдвигаются выше линии пола (dmy-7), поэтому heal с запасом вверх * (63*row+34 .. +74); ширина 64 = обе ячейки. */ - heal_off(x, 63 * row + 34, 64, 40); + pop_heal_off(x, 63 * row + 34, 64, 40); gfx_set_bank(GFX_BANK_SPRITE); draw_tile(row, col); if (col + 1 < 10) @@ -799,8 +577,8 @@ void pop_spike_redraw(int row, int col) __banked /* Перерисовать ЛЕВЫЙ шов (col0) на back-странице по живому modif соседа — * ворота соседней комнаты слева (portcullis room8) видны в col0 текущей - * комнаты через draw_gate_back (draw_tile ветка lcode==4, lmod = g_lbg[0]). - * g_lbg (lcol_bg) должен указывать на живой modif (roomtest обновляет из + * комнаты через 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+ОЗУ-копию. */ @@ -828,25 +606,25 @@ void pop_room_redraw_seam_left(void) __banked void pop_ceil_shake_draw(int col) __banked { - int x = COL_XH[col] * 8; - heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */ + int x = POP_COL_XH[col] * 8; + pop_heal_off(x, 0, 64, CEIL_BAND_H); /* фон полосы обоих тайлов */ gfx_set_bank(GFX_BANK_SPRITE); - g_clip_top = POP_YOFF; + pop_t_clip_top = POP_YOFF; draw_tile(-1, col); if (col + 1 < 10) draw_tile(-1, col + 1); - g_clip_top = 0; + pop_t_clip_top = 0; gfx_set_bank(GFX_BANK_NORMAL); } void pop_ceil_bake_empty(int col) __banked { - int x = COL_XH[col] * 8; + 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); - g_clip_top = POP_YOFF; - bake_rest = 1; /* фон = покой (см. bake_rest) */ + pop_t_clip_top = POP_YOFF; + pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ /* Восстанавливаем ВСЁ, чья графика попадает в стёртую полосу, а не только * два тайла ряда −1 (BUG-LOOSE-3). Куски тайла рисуются ВВЕРХ от своей * нижней грани и свисают ВПРАВО, поэтому в полосу над тайлом col лезут: @@ -863,8 +641,8 @@ void pop_ceil_bake_empty(int col) __banked for (c = c0; c <= c1; c++) draw_tile(-1, c); for (c = c0; c <= c1; c++) draw_tile(0, c); } - bake_rest = 0; - g_clip_top = 0; + pop_t_bake_rest = 0; + pop_t_clip_top = 0; gfx_set_bank(GFX_BANK_NORMAL); } @@ -877,10 +655,10 @@ void pop_ceil_bake_empty(int col) __banked * (pot_size<<2) − 14 (seg008:668). */ void pop_torch_draw(int row, int col, uint8_t modif) __banked { - /* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = COL_XH[col+1] + 1 + /* Пламя рисует ПРАВЫЙ сосед со своим draw_xh: xh = POP_COL_XH[col+1] + 1 * (seg008:560), т.е. x = (col+1)*32 + 8 — как и основание факела (env * 146), которое тоже лежит в ячейке соседа. */ - int x = (col + 1 < 10 ? COL_XH[col + 1] : COL_XH[col] + 4) * 8 + 8; + 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 @@ -888,7 +666,7 @@ void pop_torch_draw(int row, int col, uint8_t modif) __banked * непрозрачный блит сам полностью накрывает предыдущий кадр. Кадры * упакованы с opaque=True (pop_pack_bg POT_FLAME_IDS). */ gfx_set_bank(GFX_BANK_SPRITE); - pot_b((uint8_t)(modif + 1), x, yb); + pop_pot_b((uint8_t)(modif + 1), x, yb); gfx_set_bank(GFX_BANK_NORMAL); } @@ -896,13 +674,13 @@ 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 = (COL_XH[col] + 3) * 8 + 1; + int x = (POP_COL_XH[col] + 3) * 8 + 1; int yb = 63 * row + 62 - (pot_size << 2) - 14; if (type == 0) return; - heal_off(x - 1, yb - 10, 8, 12); + pop_heal_off(x - 1, yb - 10, 8, 12); gfx_set_bank(GFX_BANK_SPRITE); - pot_b(POT_MASK_ID, x, yb); /* маска (под пузырьком) */ - pot_b(POTION_FRAM_BUBB[modif & 7], x, yb); /* кадр пузырька */ + pop_pot_b(POT_MASK_ID, x, yb); /* маска (под пузырьком) */ + pop_pot_b(POTION_FRAM_BUBB[modif & 7], x, yb); /* кадр пузырька */ gfx_set_bank(GFX_BANK_NORMAL); } @@ -912,7 +690,7 @@ void pop_potion_draw(int row, int col, uint8_t modif) __banked * в пол. Звать из pop_process_trobs для кнопок текущей комнаты. */ void pop_floor_bake(int row, int col) __banked { - int x = COL_XH[col] * 8; + int x = POP_COL_XH[col] * 8; int yb = POP_YOFF + 63 * row; /* Wipe СВОЕЙ ячейки + правой грани (заходит в соседа) + перерисовка пола. * НИЗ строго yb+64 (≈ низ передней грани пола): ниже — СТЕНА ряда ниже @@ -921,10 +699,10 @@ void pop_floor_bake(int row, int col) __banked setfillstyle(SOLID_FILL, BLACK); bar(x, yb + 26, x + 59, yb + 64); gfx_set_bank(GFX_BANK_TRANSPARENT); - bake_rest = 1; /* фон = покой (см. bake_rest) */ + pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ draw_tile(row, col); if (col + 1 < 10) draw_tile(row, col + 1); - bake_rest = 0; + pop_t_bake_rest = 0; gfx_set_bank(GFX_BANK_NORMAL); } @@ -960,7 +738,7 @@ void pop_gate_redraw(int row, int col) __banked { int x, yt; if (col + 1 > 9 || row < 0 || row > 2) return; - x = COL_XH[col + 1] * 8; + x = POP_COL_XH[col + 1] * 8; yt = POP_YOFF + 63 * row; gfx_set_bank(GFX_BANK_NORMAL); setfillstyle(SOLID_FILL, BLACK); @@ -973,13 +751,13 @@ void pop_gate_redraw(int row, int col) __banked /* Запечь ПУСТОТУ на месте упавшего loose (текущая страница). Порт * redraw_at_cur_mob (seg007:132C): перерисовать сам тайл (теперь empty) И * тайл СПРАВА (его левая грань зависит от нашего кода: пока был loose — - * рисовал loose-правый-край env_b(42); теперь пусто → чистая грань). - * g_fg[pos] уже = 0 (pop_map). Чёрная плита (NORMAL) стирает старые + * рисовал 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 = COL_XH[col] * 8; - int xr = COL_XH[col + 1] * 8; + int x = POP_COL_XH[col] * 8; + int xr = POP_COL_XH[col + 1] * 8; int yb = 63 * row + 46; /* Чёрная плита (opaque NORMAL — пишет в VRAM И ОЗУ-копию акселератора): * стирает ЗАПЕЧЁННЫЙ пиксель-в-пиксель loose-пол с обоих тайлов, иначе @@ -999,15 +777,15 @@ void pop_loose_bake_empty(int row, int col) __banked if (col + 1 < 10) bar(xr, yb, xr + 31, POP_YOFF + 63 * row + 65); /* сосед: РОВНО 32 — не залезть в col+2 (его не перерисовываем) */ /* Новый статич. фон: сам тайл (empty → «стена вниз» topright комнаты - * снизу) + правый сосед (его левая грань: был loose-край env_b(42), + * снизу) + правый сосед (его левая грань: был loose-край pop_env_b(42), * теперь пусто). TRANSPARENT — 0xFF не забивает чёрный, пишет и в * ОЗУ-копию → heal сохранит. */ gfx_set_bank(GFX_BANK_TRANSPARENT); - bake_rest = 1; /* фон = покой (см. bake_rest) */ + pop_t_bake_rest = 1; /* фон = покой (см. pop_t_bake_rest) */ draw_tile(row, col); if (col + 1 < 10) draw_tile(row, col + 1); - bake_rest = 0; + pop_t_bake_rest = 0; gfx_set_bank(GFX_BANK_NORMAL); } @@ -1029,7 +807,7 @@ typedef struct { uint8_t active; /* 0 нет; 1 падает */ uint8_t clean; /* кадров дочистки после конца полёта (2 = обе стр.) */ uint8_t defer; /* кусок ниже Kid → рисуем ПОСЛЕ него (y-порядок) */ - int x; /* левый край (COL_XH*8) */ + int x; /* левый край (POP_COL_XH*8) */ int y; /* = curmob.y оригинала (низ куска, dby), растёт вниз */ int prev_y[2]; /* последняя нарисованная y на КАЖДОЙ странице (heal) */ int8_t speed; @@ -1059,11 +837,11 @@ void pop_loose_mob_spawn(int row, int col) __banked for (i = 0; i < MOB_MAX; i++) if (!mobs[i].active && !mobs[i].clean) { m = &mobs[i]; break; } if (!m) return; /* таблица полна — кусок без спрайта (тайл всё равно пуст) */ - m->x = COL_XH[col] * 8; + 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 = g_room; /* curmob.room (add_mob, seg007:1010) */ + m->room = pop_t_room; /* curmob.room (add_mob, seg007:1010) */ m->speed = 0; m->active = 1; m->clean = 0; @@ -1082,7 +860,7 @@ void pop_loose_mob_spawn(int row, int col) __banked /* Остановить падающий кусок (при смене комнаты — состояние per-tile не * должно утечь в новую комнату). */ /* Тайл ОБЪЕКТА Kid (set_objtile_at_char, seg006:1833) — в момент обработки - * этого тайла спрайт Kid попадает в midtable. Задаётся в pop_fore_over_kid, + * этого тайла спрайт Kid попадает в midtable. Задаётся в pop_fore_over_char, * читается other_overlay_tile (порядок «кто поверх кого»). */ static int8_t obj_col, obj_row; @@ -1118,7 +896,7 @@ void pop_loose_mob_room_changed(void) __banked * и соседей), в чужой — прямо из данных уровня. */ static uint8_t mob_tile_at(mob_t *m) { - if (m->room == g_room) return (uint8_t)(tile_code(m->row, m->col) & 0x1F); + if (m->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); } @@ -1127,13 +905,13 @@ 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 == g_room); + here = (uint8_t)(m->room == pop_t_room); /* Стереть кусок, нарисованный на ЭТОЙ странице в прошлый раз (у каждой * страницы своя копия фона). Раньше коридор был фиксирован под ряд 2; * плита-потолок падает через весь ряд 0, поэтому чистим по фактической * прошлой позиции. */ if (here && m->prev_y[pg] != MOB_Y_NONE) { - heal_off(MOB_X0(m->x), m->prev_y[pg] - 27, MOB_W, 32); + 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; } @@ -1184,7 +962,7 @@ static void mob_tick_one(mob_t *m, uint8_t pg) 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 > kid_fp_obj_y()); + else over = (m->y > pop_cd[POP_CH_KID].fpy); if (over) { m->defer = 1; return; } } mob_render(m, pg); @@ -1203,9 +981,9 @@ static void mob_render(mob_t *m, uint8_t pg) * 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); - env_b(43, m->x, m->y); - env_b(41, m->x, m->y - 3); - env_b(42, m->x + 32, m->y - 1); + 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). @@ -1224,13 +1002,13 @@ static void mob_render(mob_t *m, uint8_t pg) * границе рядов. Порт 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) g_clip_top = POP_YOFF; /* полоса у потолка — только срез */ + if (r < 0) pop_t_clip_top = POP_YOFF; /* полоса у потолка — только срез */ draw_tile(r, m->col + 1); - g_clip_top = 0; + pop_t_clip_top = 0; if (rt != r) { - if (rt < 0) g_clip_top = POP_YOFF; + if (rt < 0) pop_t_clip_top = POP_YOFF; draw_tile(rt, m->col + 1); - g_clip_top = 0; + pop_t_clip_top = 0; } } gfx_set_bank(GFX_BANK_NORMAL); @@ -1239,7 +1017,7 @@ static void mob_render(mob_t *m, uint8_t pg) } /* Отложенная отрисовка куска ПОВЕРХ Kid (он ниже Kid по obj_y). Звать - * после kid_draw, до fore-слоя. */ + * после отрисовки персонажей, до fore-слоя. */ void pop_loose_mob_draw_over(void) __banked { uint8_t pg = gfx_get_draw_page() & 1, i; @@ -1256,7 +1034,7 @@ uint8_t pop_loose_mob_pos(int *col, int *y) __banked * curmob.room: кусок, падающий в покинутой комнате, Киду по голове не * бьёт. */ for (i = 0; i < MOB_MAX; i++) - if (mobs[i].active && mobs[i].room == g_room) { + if (mobs[i].active && mobs[i].room == pop_t_room) { *col = mobs[i].col; *y = mobs[i].y; return 1; } return 0; @@ -1299,14 +1077,14 @@ void pop_room_draw(uint8_t room_num, const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked { int row, col; - g_room = room_num; + pop_t_room = room_num; wall_pattern_reset(); /* кэш кладки привязан к номеру комнаты */ - g_fg = fg; g_bg = bg; - g_lfg = lcol_fg; g_lbg = lcol_bg; + 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); - bake_rest = 1; /* фон = покой (см. bake_rest) */ + 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); @@ -1314,11 +1092,11 @@ void pop_room_draw(uint8_t room_num, * при draw_main_y=-1): нижние/правые грани ряда 2 комнаты сверху. Кадры * высокие — верх ушёл бы в борт (за поле); клип по верхней границе поля * (POP_YOFF) рисует ТОЛЬКО срез у потолка, без зачернения борта. */ - g_clip_top = POP_YOFF; + pop_t_clip_top = POP_YOFF; for (col = 0; col < 10; col++) draw_tile(-1, col); - g_clip_top = 0; - bake_rest = 0; + pop_t_clip_top = 0; + pop_t_bake_rest = 0; gfx_set_bank(GFX_BANK_NORMAL); } @@ -1327,7 +1105,7 @@ void pop_room_draw(uint8_t room_num, * — рядов top..bottom × колонок left..right (<= 2×2 = 4 тайла) — рисуется * ПОВЕРХ Kid: то, что по изометрии перед ним (передние грани колонн/ворот/ * большой колонны/щебня). Стены и факелы имеют fore_id=0 → остаются сзади. - * Рисуем в GFX_BANK_SPRITE (видео-ОЗУ, 0xFF прозрачен); kid_heal восстановит + * Рисуем в GFX_BANK_SPRITE (видео-ОЗУ, 0xFF прозрачен); heal персонажа восстановит * из теневого фона (fore в нём уже запечён pop_room_draw). obj_x — ЛОГИЧЕСКАЯ * X кадра Kid (до ×8/7), obj_y — низ спрайта, w/h — размер, dir — направление. */ static int8_t col_from_x(int xpos) /* get_tile_div_mod: колонка (58/14) */ @@ -1344,26 +1122,26 @@ static void fore_only_tile(int row, int col) { /* Нажатая кнопка → пол/stuck (get_tile_to_draw), иначе поверх Kid лезет * передняя грань КНОПКИ. */ - uint8_t code = tile_code_drawn(row, col); + uint8_t code = pop_tile_code_drawn(row, col); const piece *t; - t = &tile_table[code]; - int x = COL_XH[col] * 8; + t = &pop_tile_table[code]; + int x = POP_COL_XH[col] * 8; int dmy = 63 * row + 62; if (code == 20) { - wall_b(WALL_FRAM_MAIN[wall_modifier(row, col)], x, dmy); + pop_wall_b(POP_WALL_FRAM_MAIN[pop_wall_modifier(row, col)], x, dmy); wall_pattern(row, col, 1); return; } if (code == 2) { /* пики: fore-кадр */ - uint8_t sm = tile_mod(row, col); + uint8_t sm = pop_tile_mod(row, col); uint8_t sf = (sm & 0x80) ? 5 : sm; if (sf > 0 && sf < 10 && SPIKES_FRAM_FORE[sf]) - env_b(SPIKES_FRAM_FORE[sf], x, dmy - 2); + pop_env_b(SPIKES_FRAM_FORE[sf], x, dmy - 2); } if (code == 10) { /* зелье: склянка (пузырёк — динамика) */ - potion_flask(row, col, tile_mod(row, col)); + pop_potion_flask(row, col, pop_tile_mod(row, col)); } else if (t->fore_id) { - fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); + pop_fore_b(t->fore_id, t->fore_x * 8 + x, t->fore_y + dmy); } } @@ -1375,23 +1153,23 @@ static void fore_only_tile(int row, int col) * backtable — под персонажем. */ static void ceil_over_kid_tile(int col) { - uint8_t code = tile_code(-1, col); - const piece *t = &tile_table[code]; - int x = COL_XH[col] * 8; + uint8_t code = pop_tile_code(-1, col); + const piece *t = &pop_tile_table[code]; + int x = POP_COL_XH[col] * 8; int dby = 63 * (-1) + 65; /* = 2 */ if (code == 20) { /* draw_tile_bottom: стена */ - wall_b(WALL_FRAM_BOTTOM[wall_modifier(-1, col)], x, dby); + pop_wall_b(POP_WALL_FRAM_BOTTOM[pop_wall_modifier(-1, col)], x, dby); wall_pattern(-1, col, 0); } else if (t->bottom_id) { - env_b(t->bottom_id, x, dby); + pop_env_b(t->bottom_id, x, dby); } if (code == 11) /* draw_loose(1): плита-потолок */ - env_b(LOOSE_FRAM_BOTTOM[get_loose_frame(pop_ceil_modif[col])], x, dby); + pop_env_b(POP_LOOSE_FRAM_BOTTOM[pop_loose_frame(pop_ceil_modif[col])], x, dby); fore_only_tile(-1, col); /* draw_tile_fore */ } /* Пересекается ли габарит тайла с окном fore-слоя. Шаг тайла — 32 px - * (COL_XH[col]*8 = col*32), куски выходят вправо максимум на +8 (дивайдеры + * (POP_COL_XH[col]*8 = col*32), куски выходят вправо максимум на +8 (дивайдеры * и метки рисуются в ячейке правого соседа) и вверх на 42+высота кадра. * Даёт отсев ЦЕЛОГО тайла ДО обращения к атласу: спрайт 27..40 px шириной * задевает не все тайлы футпринта, а платили за каждый (atlas_image + @@ -1399,11 +1177,11 @@ static void ceil_over_kid_tile(int col) static uint8_t tile_in_fclip(int row, int col) { int x0, y0; - if (!fclip_on) return 1; - x0 = COL_XH[col] * 8; + if (!pop_t_fclip_on) return 1; + x0 = POP_COL_XH[col] * 8; y0 = POP_YOFF + 63 * row; - return (uint8_t)(x0 < fclip_x1 && x0 + 40 > fclip_x0 && - y0 - 8 < fclip_y1 && y0 + 70 > fclip_y0); + return (uint8_t)(x0 < pop_t_fclip_x1 && x0 + 40 > pop_t_fclip_x0 && + y0 - 8 < pop_t_fclip_y1 && y0 + 70 > pop_t_fclip_y0); } static void fore_tile(int row, int col) /* передний слой одного тайла */ @@ -1417,9 +1195,9 @@ static void fore_tile(int row, int col) /* передний слой одно * висящего/подтягивающегося Kid видны НАД плитой, а тело уходит ЗА неё. * Без этого Kid лез на плиту «с переднего края» (комната 12: вис на * кромке loose над дырой от соседней упавшей плиты). */ - if (row >= 0 && tile_code_drawn(row, col) == 11) - env_b(LOOSE_FRAM_BOTTOM[get_loose_frame(pop_loose_modif[row * 10 + col])], - COL_XH[col] * 8, 63 * row + 65); + if (row >= 0 && pop_tile_code_drawn(row, col) == 11) + pop_env_b(POP_LOOSE_FRAM_BOTTOM[pop_loose_frame(pop_loose_modif[row * 10 + col])], + POP_COL_XH[col] * 8, 63 * row + 65); /* draw_tile_fore (seg008:690) — ТОЛЬКО add_foretable-часть. Раньше здесь * рисовалась ещё и передняя кромка пола (bottom_id) «чтобы перекрывала * Kid», но в оригинале bottom идёт через draw_tile_bottom в backtable @@ -1443,10 +1221,10 @@ static void climb_overlay_tile(int row, int col, uint8_t fidx) * Kid спускается/подтягивается, закрашивается целым тайлом */ uint8_t code; if (!tile_in_fclip(row, col)) return; - code = tile_code_drawn(row, col); + code = pop_tile_code_drawn(row, col); int x, dmy, dby; const piece *t; - if (tile_code(row, col - 1) != 0) return; /* слева не пусто → не кромка */ + if (pop_tile_code(row, col - 1) != 0) return; /* слева не пусто → не кромка */ if (code != 1 && code != 3 && code != 5 && code != 19) { /* seg008:1E3A draw_floor_overlay: floor-оверлей рисуется только для * floor/pillar/stuck/torch, а для ОСТАЛЬНЫХ тайлов вызывается @@ -1455,13 +1233,13 @@ static void climb_overlay_tile(int row, int col, uint8_t fidx) other_overlay_tile(row, col); return; } - x = COL_XH[col] * 8; + x = POP_COL_XH[col] * 8; dmy = 63 * row + 62; dby = dmy + 3; - t = &tile_table[code]; + t = &pop_tile_table[code]; ov_mark(x, 63 * row + 2, 64, 64); - env_b(FLOOR_LEFT_OVERLAY[fidx], x, dmy + (code == 5 ? 1 : 0)); - if (t->bottom_id) env_b(t->bottom_id, x, dby); /* draw_tile_bottom */ + pop_env_b(FLOOR_LEFT_OVERLAY[fidx], x, dmy + (code == 5 ? 1 : 0)); + if (t->bottom_id) pop_env_b(t->bottom_id, x, dby); /* draw_tile_bottom */ } @@ -1475,42 +1253,42 @@ static void climb_overlay_tile(int row, int col, uint8_t fidx) * draw_tile_base, draw_tile_anim (свои пики) и draw_tile_bottom. */ static void overlay_mid_tile(int row, int col) { - uint8_t code = tile_code_drawn(row, col); - uint8_t lcode = tile_code_drawn(row, col - 1); + uint8_t code = pop_tile_code_drawn(row, col); + uint8_t lcode = pop_tile_code_drawn(row, col - 1); const piece *t; - int x = COL_XH[col] * 8; + int x = POP_COL_XH[col] * 8; int dby = 63 * row + 65; int dmy = dby - 3; - t = &tile_table[code]; + t = &pop_tile_table[code]; ov_mark(x, 63 * row + 2, 64, 64); /* тайл + правый сосед (грани) */ if (code != 20 && lcode == 1) /* draw_tile_right: только 42 */ - env_b(42, x, tile_table[1].right_y + dmy); + pop_env_b(42, x, pop_tile_table[1].right_y + dmy); { /* draw_tile_base (seg008:0A8E) — ЦЕЛИКОМ, вместе с подстановками id: - * у loose верх плиты берётся из loose_fram_left (в tile_table base_id=0), + * у loose верх плиты берётся из loose_fram_left (в pop_tile_table base_id=0), * у opener'а без пола слева — 148. Раньше здесь стоял голый base_id, и * верх loose-плиты в оверлей не попадал: передняя грань ложилась поверх * Kid (foretable), а сама плита — нет, и подтягивающийся Kid рисовался * поверх её верхней плоскости (комната 12). */ uint8_t base_id = t->base_id; if (code == 11 && row >= 0) - base_id = LOOSE_FRAM_LEFT[get_loose_frame(pop_loose_modif[row * 10 + col])]; + base_id = POP_LOOSE_FRAM_LEFT[pop_loose_frame(pop_loose_modif[row * 10 + col])]; else if (code == 0x0F && lcode == 0) base_id = 148; if (base_id) - env_b(base_id, x, t->base_y + dmy); + pop_env_b(base_id, x, t->base_y + dmy); } if (code == 2) { /* draw_tile_anim: свои пики */ - uint8_t sm = tile_mod(row, col); + uint8_t sm = pop_tile_mod(row, col); uint8_t sf = (sm & 0x80) ? 5 : sm; - if (sf > 0 && sf < 10 && SPIKES_FRAM_LEFT[sf]) - env_b(SPIKES_FRAM_LEFT[sf], x, dmy - 2); + if (sf > 0 && sf < 10 && POP_SPIKES_FRAM_LEFT[sf]) + pop_env_b(POP_SPIKES_FRAM_LEFT[sf], x, dmy - 2); } if (code == 20) { /* draw_tile_bottom */ - uint8_t wmod = wall_modifier(row, col); - wall_b(WALL_FRAM_BOTTOM[wmod], x, dby); + uint8_t 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) { - env_b(t->bottom_id, x, dby); + pop_env_b(t->bottom_id, x, dby); } } @@ -1535,7 +1313,7 @@ static void other_overlay_tile(int row, int col) { uint8_t code, left; if (!tile_in_fclip(row, col)) return; - code = tile_code(row, col); + code = pop_tile_code(row, col); /* Пустой тайл НЕ окклюдирует: у него нет тела пола/стены, а draw_tile для * него рисует лишь ФОНОВУЮ сетку (BLUELINE «силуэт кладки» — правая грань * пустого соседа). В оригинале она уходит в y-сортируемый midtable и @@ -1545,9 +1323,9 @@ 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; - left = tile_code(row, col - 1); + left = pop_tile_code(row, col - 1); if (left == 0) { overlay_mid_tile(row, col); return; } /* ветка 1: сосед слева пуст */ - if (col > 0 && tile_code(row, col - 2) == 0) { + if (col > 0 && pop_tile_code(row, col - 2) == 0) { /* Ветка 2 (тайл ЧЕРЕЗ ОДИН слева пуст): в оригинале она ставит * tile_object_redraw[tilepos]=0xFF, т.е. объекты тайлов tilepos−1 и * tilepos рисуются СРАЗУ ПОСЛЕ оверлея — Kid возвращается наверх. @@ -1559,9 +1337,8 @@ static void other_overlay_tile(int row, int col) } /* Футпринт персонажа в тайлах (порт set_char_collision, seg006:0723). - * Выделено из pop_fore_over_kid, потому что тем же счётом пользуется страж - * (pop_fore_over_char): в оригинале это общий код для любого Char. - * Результат — в файловых статиках, а не через 6 указателей: на Z80 такой + * Общий счёт для любого Char (Кид, страж, скелет) — в оригинале это один + * код на всех. Результат — в файловых статиках, а не через 6 указателей: на Z80 такой * возврат стоил бы дороже самой функции. */ static int8_t fp_cL, fp_cR, fp_cLraw, fp_rT, fp_rB, fp_rTraw; @@ -1611,20 +1388,62 @@ static void char_footprint(int obj_x, int obj_y, uint16_t w, uint16_t h, fp_rT = rT; fp_rB = rB; fp_rTraw = rTraw; } -void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, - int8_t dir, uint8_t frame, uint8_t action) __banked +/* Передние грани тайлов и оверлеи ПОВЕРХ персонажа — ОДИН проход на всех + * Char. В оригинале add_kid_to_objtable (seg008:22F0) и + * add_guard_to_objtable (seg008:2324) имеют ИДЕНТИЧНОЕ тело и различаются + * только окном (loadkid/loadshad), типом объекта и спецкейсом тени у + * зеркала; redraw_at_char / redraw_at_char2 (seg003:0576/0645) гейтов по + * charid не имеют вовсе. + * + * До DRAW-CHAR тут стояли ДВА прохода — свой у Кида, свой у соперника, — и + * расхождение уже стоило бага: падающий скелет рисовался поверх верхней + * грани пола соседней колонки, потому что порт redraw_at_char2 был только у + * Кида. Тем же путём пошли бы тень (ур. 4/5/6/12), визирь (13) и толстяк + * (12) — поэтому проход сведён к одному. + * + * obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ спрайта, w/h — размер + * кадра; всё остальное берётся из самого персонажа (ch). Окно fore-клипа к + * этому моменту уже стоит: его задаёт тот, кто нарисовал спрайт. */ +void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y, + uint16_t w, uint16_t h) __banked { int8_t cL, cR, cLraw, rT, rB, rTraw, r, c; - fclip_on = 1; /* окно = прямоугольник спрайта */ - char_footprint(obj_x, obj_y, w, h, dir, Kid.sword); + uint8_t frame = ch->frame, action = ch->action; + pop_t_fclip_on = 1; /* окно = прямоугольник спрайта */ + char_footprint(obj_x, obj_y, w, h, ch->direction, ch->sword); + /* Футпринт считается по КАДРУ персонажа (char_x_left/right, seg006:1021) + * и потому УЖЕ спрайта, а рядом с ним рисуются ещё клинок и «брызги» + * удара — они уходят ВПЕРЁД-ВВЕРХ и запросто попадают в соседнюю + * колонку или ряд выше. Оригинал этим не страдает: он помечает тайлы + * на перерисовку и рисует foretable целиком, а у нас проход сужен окном + * ради скорости (см. pop_fore_layer_cost). Поэтому перебор расширяется + * САМИМ ОКНОМ fore-клипа — оно и есть объединение «персонаж + клинок + + * брызги», считать его второй раз не надо. */ + if (pop_t_fclip_x1 > pop_t_fclip_x0 && pop_t_fclip_y1 > pop_t_fclip_y0) { + int8_t c2, r2; + for (c2 = 0; c2 <= 9; c2++) { + int x0 = POP_COL_XH[c2] * 8; + if (x0 + 40 > pop_t_fclip_x0 && x0 < pop_t_fclip_x1) { + if (c2 < fp_cL) fp_cL = c2; + if (c2 > fp_cR) fp_cR = c2; + } + } + for (r2 = 0; r2 <= 2; r2++) { + int y0 = POP_YOFF + 63 * r2; + if (y0 + 70 > pop_t_fclip_y0 && y0 - 8 < pop_t_fclip_y1) { + if (r2 < fp_rT) fp_rT = r2; + if (r2 > fp_rB) fp_rB = r2; + } + } + } cL = fp_cL; cR = fp_cR; cLraw = fp_cLraw; rT = fp_rT; rB = fp_rB; rTraw = fp_rTraw; /* set_objtile_at_char (seg006:1833): тайл, при обработке которого спрайт - * Kid попадает в midtable — база порядка окклюзии в other_overlay_tile. + * попадает в midtable — база порядка окклюзии в other_overlay_tile. * run_jump (action 1) — нижний ряд/левая колонка габарита; вис/полёт/ * подъём — колонка на одну ЛЕВЕЕ curr_col. */ - obj_row = Kid.curr_row; - obj_col = Kid.curr_col; + obj_row = ch->curr_row; + obj_col = ch->curr_col; if (action == 1) { obj_row = rB; obj_col = cL; } if ((frame >= 135 && frame < 149) || action == 2 || action == 3 || action == 4 || action == 6) @@ -1632,16 +1451,16 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, heal восстановит */ /* Шовная окклюзия ворот (порт redraw_at_char для шва): футпринт зашёл за * ЛЕВЫЙ шов (cLraw<0) и у соседа слева в этом ряду решётка ворот (код 4) — - * перерисовать её бары ПОВЕРХ Kid. Иначе Kid, стоя за закрытыми воротами - * (col-1), рисуется поверх решётки. Бары = draw_gate_back по живому modif - * соседа (g_lbg = lcol_bg, openness). */ - if (cLraw < 0 && g_lfg) + * перерисовать её бары ПОВЕРХ персонажа. Иначе он, стоя за закрытыми + * воротами (col-1), рисуется поверх решётки. Бары = draw_gate_back по + * живому modif соседа (pop_t_lbg = lcol_bg, openness). */ + if (cLraw < 0 && pop_t_lfg) for (r = rT; r <= rB; r++) - if ((g_lfg[r] & 0x1F) == 4) { + if ((pop_t_lfg[r] & 0x1F) == 4) { int dby = 63 * r + 65; - draw_gate_back(g_lbg ? g_lbg[r] : 0, COL_XH[0], dby, dby - 3); + draw_gate_back(pop_t_lbg ? pop_t_lbg[r] : 0, POP_COL_XH[0], dby, dby - 3); } - if (frame >= 137 && frame <= 144) { /* подъём: пол назначения поверх Kid */ + if (frame >= 137 && frame <= 144) { /* подъём: пол назначения поверх него */ uint8_t fidx = (uint8_t)(frame - 137); for (r = rT; r <= rB; r++) for (c = cL; c <= cR; c++) @@ -1678,89 +1497,13 @@ void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, * куда set_redraw* уводит отрицательные tilepos). Без этого руки Kid, * прыгающего в ряду 0, лезли поверх потолка. */ if (rTraw < 0) { - g_clip_top = POP_YOFF; + pop_t_clip_top = POP_YOFF; for (c = cL; c <= cR; c++) { - ov_mark(COL_XH[c] * 8, 0, 64, CEIL_BAND_H); + ov_mark(POP_COL_XH[c] * 8, 0, 64, CEIL_BAND_H); ceil_over_kid_tile(c); } - g_clip_top = 0; + pop_t_clip_top = 0; } gfx_set_bank(GFX_BANK_NORMAL); - fclip_on = 0; -} - -/* Передние грани тайлов поверх ПРОИЗВОЛЬНОГО персонажа (страж, дальше — - * тень/скелет). В оригинале любой Char — запись midtable, а foretable - * рисуется после всех midtable (draw_tile_fore, seg008:690), поэтому - * колонна и кромка пола перекрывают всех одинаково. - * - * Отличие от pop_fore_over_kid: только слой fore (+ полоса потолка). - * Оверлеи тела пола (draw_other_overlay) завязаны на позы Kid — вис, - * полёт, подъём, — которых у стража нет: он ходит и дерётся на полу - * (seq_77..seq_84). Появятся падающие/висящие стражи — тогда и общий - * порт redraw_at_char2, а не догадки сейчас. */ -void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h, - int8_t dir, uint8_t sword, - int ex, int ey, uint16_t ew, uint16_t eh, - uint8_t action, int8_t ccol, int8_t crow) __banked -{ - int8_t r, c; - fclip_on = 1; /* окно = прямоугольник спрайта */ - char_footprint(obj_x, obj_y, w, h, dir, sword); - /* Футпринт считается по КАДРУ персонажа (как char_x_left/right, - * seg006:1021), но рядом с ним рисуются ещё клинок и «брызги» удара — - * они уходят ВПЕРЁД-ВВЕРХ и запросто попадают в соседнюю колонку или - * ряд выше. Оригинал этим не страдает: он помечает тайлы на перерисовку - * и рисует foretable целиком, а у нас проход сужен окном ради скорости - * (см. pop_fore_layer_cost). Поэтому расширяем перебор объединённым - * ЭКРАННЫМ прямоугольником «персонаж + клинок + брызги»: иначе кусок - * меча или звёздочка удара остаются поверх кладки. */ - if (ew && eh) { - int8_t c2, r2; - for (c2 = 0; c2 <= 9; c2++) { - int x0 = COL_XH[c2] * 8; - if (x0 + 40 > ex && x0 < ex + (int)ew) { - if (c2 < fp_cL) fp_cL = c2; - if (c2 > fp_cR) fp_cR = c2; - } - } - for (r2 = 0; r2 <= 2; r2++) { - int y0 = POP_YOFF + 63 * r2; - if (y0 + 70 > ey && y0 - 8 < ey + (int)eh) { - if (r2 < fp_rT) fp_rT = r2; - if (r2 > fp_rB) fp_rB = r2; - } - } - } - gfx_set_bank(GFX_BANK_SPRITE); - /* Порядок midtable — общий для ЛЮБОГО Char (redraw_needed_tiles, - * seg008:1B06): персонаж вставляется в очередь при обработке СВОЕГО - * тайла, поэтому тайлы, идущие в обходе позже, ложатся ПОВЕРХ него. У - * Кида это уже эмулируется other_overlay_tile, а у соперника прохода не - * было вовсе — падающий скелет оказывался поверх верхней грани пола - * соседней колонки. Гейт по action тот же, что у Кида: вис/полёт/ - * падение/подъём (2/3/4/6). */ - obj_row = crow; - obj_col = ccol; - if (action == 1) { obj_row = fp_rB; obj_col = fp_cL; } - if (action == 2 || action == 3 || action == 4 || action == 6) { - obj_col--; - for (c = fp_cR; c >= fp_cL; c--) { - if (action != 2) other_overlay_tile(fp_rB, c); - if (fp_rT != fp_rB) other_overlay_tile(fp_rT, c); - } - } - for (r = fp_rT; r <= fp_rB; r++) - for (c = fp_cL; c <= fp_cR; c++) - fore_tile(r, c); - if (fp_rTraw < 0) { - g_clip_top = POP_YOFF; - for (c = fp_cL; c <= fp_cR; c++) { - ov_mark(COL_XH[c] * 8, 0, 64, CEIL_BAND_H); - ceil_over_kid_tile(c); - } - g_clip_top = 0; - } - gfx_set_bank(GFX_BANK_NORMAL); - fclip_on = 0; + pop_t_fclip_on = 0; } diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index 254ab50..463610d 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -12,6 +12,7 @@ #define POP_BG_H #include +#include "pop_char.h" /* pop_char_t — общий проход отрисовки поверх Char */ /* ---- Вертикальный layout (PoP-экран 320×200 в нашем 320×256) --------- * * Игровой экран 200px центрируем по высоте: сдвиг всего рисунка вниз на @@ -51,47 +52,40 @@ void pop_room_draw(uint8_t room_num, * room_B(0, col-1). ОДИННАДЦАТЬ байт: [0..9] — колонки комнаты снизу, * [10] — тайл (0,9) комнаты СНИЗУ-СЛЕВА (сосед угла (2,0); см. * load_rowbelow, seg008:368). 0 — нет комнаты снизу. */ -void pop_room_set_below(const uint8_t *below_row0_fg) __banked; +/* НЕ __banked: тело в резиденте (pop_tile.c) — из банка зовётся прямым call. */ +void pop_room_set_below(const uint8_t *below_row0_fg); /* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы * кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет * комнаты сверху (кромка уровня → пол). */ -void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked; +void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod); -/* Перерисовать передний слой (fore) тайлов футпринта Kid ПОВЕРХ него — - * звать ПОСЛЕ kid_draw. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ - * спрайта, w/h — размер кадра, dir — направление Kid (<0 влево). Метрики - * берутся из pop_kid (kid_fp_*). См. redraw_at_char/set_char_collision. */ -void pop_fore_over_kid(int obj_x, int obj_y, uint16_t w, uint16_t h, - int8_t dir, uint8_t frame, uint8_t action) __banked; +/* Перерисовать передний слой (fore) и оверлеи тайлов ПОВЕРХ персонажа — + * ОДИН проход на всех Char (Кид, страж, скелет, дальше тень/визирь): в + * оригинале add_kid_to_objtable и add_guard_to_objtable имеют одинаковое + * тело, а redraw_at_char/redraw_at_char2 гейтов по charid не имеют. + * Звать ПОСЛЕ отрисовки спрайта, когда окно fore-клипа уже стоит + * (pop_fore_set_clip) — перебор тайлов расширяется по нему. + * + * ch — персонаж (Kid/Guard): из него берутся direction/sword/frame/action и + * curr_col/curr_row. obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ + * спрайта, w/h — размер кадра. См. redraw_at_char/set_char_collision. */ +void pop_fore_over_char(const pop_char_t *ch, int obj_x, int obj_y, + uint16_t w, uint16_t h) __banked; /* Окно fore-слоя: прямоугольник СПРАЙТА (экранные координаты, y уже с * POP_YOFF), поверх которого fore-проход возвращает куски тайлов. Ставит - * тот, кто нарисовал спрайт (kid_draw / pop_guard_draw) — он один знает + * тот, кто нарисовал спрайт (pop_char_draw) — он один знает * итоговый габарит (у Kid — с учётом клинка и обрезки clip_char). Вне * fore-проходов окно игнорируется: фон рисуется целиком. Мотивация и * замер — в pop_bg.c у fclip_on. */ -void pop_fore_set_clip(int x, int y, int w, int h) __banked; - -/* То же для НЕ-Kid персонажа (страж): только слой fore + полоса потолка, - * без оверлеев поз виса/полёта/подъёма. obj_x — ЛОГИЧЕСКАЯ координата - * (до ×8/7), как у kid_fp_obj_x(). */ -/* __banked: отрисовка стража уехала в СВОЙ банк (pop_gdraw.c), поэтому - * вызов стал кроссбанковым и обязан идти через трамплин. Зовётся раз в - * кадр, других вызывающих нет — цена одного трамплина здесь не видна. */ -/* ex/ey/ew/eh — ОБЪЕДИНЁННЫЙ экранный прямоугольник «персонаж + клинок + - * брызги» (тот же, что уходит в pop_fore_set_clip): перебор передних тайлов - * идёт по нему, иначе меч и звёздочка удара остаются поверх кладки. - * ew == 0 — расширения нет (только габарит кадра). */ -void pop_fore_over_char(int obj_x, int obj_y, uint16_t w, uint16_t h, - int8_t dir, uint8_t sword, - int ex, int ey, uint16_t ew, uint16_t eh, - uint8_t action, int8_t ccol, int8_t crow) __banked; +void pop_fore_set_clip(int x, int y, int w, int h); /* Восстановить фон там, где в прошлых кадрах рисовался ОВЕРЛЕЙ * (draw_other_overlay/floor_overlay/полоса потолка): он выходит за - * прямоугольник Kid, поэтому kid_heal его не убирает — на второй странице - * дабл-буфера остаётся след и мерцает. Звать рядом с kid_heal. */ + * прямоугольник персонажа, поэтому его heal этого не убирает — на второй + * странице дабл-буфера остаётся след и мерцает. Звать рядом с + * pop_char_heal. */ void pop_fore_heal(void) __banked; /* Loose floors — динамическая отрисовка (состояние pop_loose_modif в @@ -154,7 +148,7 @@ void pop_loose_mob_reset(void) __banked; /* убрать все куски (С void pop_loose_mob_room_changed(void) __banked; /* Кусок НИЖЕ Kid по obj_y → рисуется ПОВЕРХ него (порт sort_curr_objs, - * seg008:1561). Звать после kid_draw, перед fore-слоем. */ + * seg008:1561). Звать после отрисовки персонажей, перед fore-слоем. */ void pop_loose_mob_draw_over(void) __banked; /* Позиция куска (колонка, y низа) для check_loose_fall_on_kid. 0 — нет. */ @@ -169,7 +163,7 @@ void pop_clip_sprite(int x, int w, int sy_top, int h) __banked; /* Почистить борта (верхний + статус-полоса/нижний) — но ТОЛЬКО в кадрах, * где кто-то реально вышел за поле (гейт по флагу от pop_clip_sprite): * даром в покое, O(1) при падении (не зависит от числа спрайтов). Звать - * КАЖДЫЙ кадр ПОСЛЕ kid_draw+fore на back-странице. */ + * КАЖДЫЙ кадр ПОСЛЕ персонажей и fore на back-странице. */ void pop_room_clip_borders(void) __banked; #endif diff --git a/applications/PoP/roomtest/pop_cdraw.c b/applications/PoP/roomtest/pop_cdraw.c new file mode 100644 index 0000000..c8b58e2 --- /dev/null +++ b/applications/PoP/roomtest/pop_cdraw.c @@ -0,0 +1,455 @@ +/* + * pop_cdraw.c — ОТРИСОВКА ПЕРСОНАЖЕЙ (Кид, страж, скелет; дальше тень, + * визирь, толстяк) + полоса HP. Банк 4. + * + * Одно тело на всех Char — порт add_kid_to_objtable/add_guard_to_objtable + * (seg008:22F0/2324), различия сведены к слоту; мотивация и порядок вызовов + * в кадре — в шапке pop_cdraw.h. До DRAW-CHAR это были kid_draw в + * резиденте и pop_guard_draw здесь же, каждый со своей математикой кадра, + * своими heal-прямоугольниками и своим проходом окклюзии. + * + * Почему банк, а не резидент: зовёт только главный цикл (раз в кадр на + * персонажа), горячих банк→банк переходов отсюда нет, а резидентное + * пространство W1+W2 делят код и данные — килобайт кода, уехавший в банк, + * это килобайт для кучи. Писучие статики банкового модуля (--dataseg не + * задаём) остаются в обычном _DATA, то есть в W2: pop_cd читают и физика, и + * фон из СВОИХ банков — без трамплина, обычной адресацией. + */ +#include +#include +#include +#include "pop_guard.h" +#include "pop_kid.h" /* атласы/кадр Кида, pop_sword_draw, окна Char */ +#include "pop_cdraw.h" +#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char, pop_fore_set_clip */ +#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ +#include "pop_map.h" /* hitp_curr/hitp_max — HP Кида; clip_char */ +#include "pop_level.h" /* тип соперника уровня (набор спрайтов) */ +#include "kid_data.h" +#include "pop_guard_pal.h" /* таблица палитр стражей (сгенерена упаковщиком) */ + +/* Цвет стража (set_chtab_palette, seg003:257). Оригинал держит ОДИН набор + * спрайтов и подменяет 16 цветов палитры chtab_5 по level.guards_color — + * так на уровне 2 стражи комнат 4/7/11/15/24 разного цвета (1/3/1/1/6). + * Полоса HP стража рисуется тем же атласом, поэтому меняется заодно. + * color = 0 — палитру не трогаем (оригинал в этом случае set_chtab_palette + * не зовёт: тип уровня не «обычный страж»). + * Грузим В ОБЕ палитры: у страниц дабл-буфера они раздельные. */ +void pop_guard_set_palette(uint8_t color) __banked +{ + /* Таблица — rodata ЭТОГО модуля, то есть страница банка (0xC000+), а + * gfx_pal_load отдаёт указатель в BIOS ($A4 через rst #0x08). BIOS + * читает только #4000-#BFFF (см. корневой CLAUDE.md), поэтому копируем + * запись в локальный буфер — стек гарантированно в W2. Без копии BIOS + * забирал мусор с чужой страницы и страж становился невидимым. */ + uint8_t buf[64]; + const uint8_t *p; + uint8_t i; + if (color == 0 || color > POP_GUARD_NPAL) return; + p = pop_guard_pal[color - 1]; + for (i = 0; i < 64; i++) buf[i] = p[i]; + gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf); + gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf); +} + +/* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */ +#define GUARD_PAGES 5 +static atlas_t gp[GUARD_PAGES]; +static uint8_t g_ok; + +/* Состояние отрисовки по слотам (см. pop_cdraw.h): heal-прямоугольники по + * страницам дабл-буфера, габарит кадра и окно fore-клипа. */ +pop_cdraw_t pop_cd[POP_CH_N]; + +/* «Брызги» урона Кида — спрайт chtab_2 id 218 (res619.png, в наших атласах + * страница 27). У соперника это image 1 его собственного набора. */ +#define KID_SPLASH_IMAGE 218 + +/* Набор спрайтов соперника выбирает ТИП УРОВНЯ (load_lev_spr, seg000:1092): + * tbl_guard_type = {0 guard, 1 fat, 2 skeleton, 3 vizier, 4 shadow, −1 нет}. + * У нас пока два набора: обычный страж (уровни 1-2) и скелет (уровень 3); + * у скелета и кадров меньше (28 против 34), поэтому пятую страницу он не + * грузит. Уровень БЕЗ соперников (type < 0) не грузит ничего. */ +int pop_guard_load(void) __banked +{ + static const char *const guard_names[GUARD_PAGES] = { + "GUARD\\g0.atl", "GUARD\\g1.atl", "GUARD\\g2.atl", + "GUARD\\g3.atl", "GUARD\\g4.atl" + }; + static const char *const skel_names[GUARD_PAGES] = { + "SKEL\\g0.atl", "SKEL\\g1.atl", "SKEL\\g2.atl", + "SKEL\\g3.atl", 0 + }; + const char *const *names; + int8_t type = pop_level_guard_type(pop_current_level); + uint8_t i; + if (type < 0) return 0; /* соперников на уровне нет */ + names = (type == 2) ? skel_names : guard_names; + for (i = 0; i < GUARD_PAGES; i++) { + if (names[i] == 0) break; + if (atlas_load(&gp[i], names[i]) != 0) return -1; + } + g_ok = 1; + /* Цвет: у скелета он СВОЙ (палитра из его набора), у стража подменяется + * из res10.bin по guards_color — это делает pop_guard_set_palette при + * входе в комнату (seg002:186: у не-guard curr_guard_color = 0). */ + if (type == 2) { + uint8_t buf[64], k; + for (k = 0; k < 64; k++) buf[k] = pop_skel_pal[k]; /* BIOS: буфер в W2 */ + gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf); + gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf); + } + return 0; +} + +void pop_guard_free(void) __banked +{ + uint8_t i; + if (!g_ok) return; + for (i = 0; i < GUARD_PAGES; i++) atlas_free(&gp[i]); + g_ok = 0; +} + +/* Добавить накладной спрайт (клинок / брызги) в overlay-прямоугольник + * страницы. Оба мелкие и в одном кадре встречаются редко — их объединение + * дешевле третьего heal-слота. */ +static void cd_overlay_add(pop_cdraw_t *s, uint8_t dp, int x, int y, + uint16_t w, uint16_t h) +{ + if (!s->ovalid[dp]) { + s->ox[dp] = x; s->oy[dp] = y; s->ow[dp] = w; s->oh[dp] = h; + s->ovalid[dp] = 1; + return; + } + { + int x0 = s->ox[dp], y0 = s->oy[dp]; + int x1 = x0 + (int)s->ow[dp], y1 = y0 + (int)s->oh[dp]; + if (x < x0) x0 = x; + if (y < y0) y0 = y; + if (x + (int)w > x1) x1 = x + (int)w; + if (y + (int)h > y1) y1 = y + (int)h; + s->ox[dp] = x0; s->oy[dp] = y0; + s->ow[dp] = (uint16_t)(x1 - x0); s->oh[dp] = (uint16_t)(y1 - y0); + } +} + +/* Добавить прямоугольник в окно fore-клипа слота (объединение «спрайт + + * накладные»): по нему fore-проход возвращает куски тайлов и по нему же + * расширяет перебор колонок/рядов. */ +static void cd_clip_add(pop_cdraw_t *s, int x, int y, uint16_t w, uint16_t h) +{ + if (!s->cw) { + s->cx = x; s->cy = y; s->cw = w; s->ch = h; + return; + } + { + int x1 = s->cx + (int)s->cw, y1 = s->cy + (int)s->ch; + if (x < s->cx) s->cx = x; + if (y < s->cy) s->cy = y; + if (x + (int)w > x1) x1 = x + (int)w; + if (y + (int)h > y1) y1 = y + (int)h; + s->cw = (uint16_t)(x1 - s->cx); + s->ch = (uint16_t)(y1 - s->cy); + } +} + +/* Стирание прошлого кадра персонажа и его накладных. pop_heal_fast, а не + * голый gfx_heal: стираем ровно то, что рисовали, а рисовали в подавляющем + * большинстве кадров noclip-ядром; клипающий путь остаётся для straddle и + * выхода за поле. */ +void pop_char_heal(uint8_t who) __banked +{ + pop_cdraw_t *s = &pop_cd[who]; + uint8_t p = gfx_get_draw_page() & 1; + if (s->valid[p]) pop_heal_fast(s->x[p], s->y[p] + POP_YOFF, s->w[p], s->h[p]); + if (s->ovalid[p]) pop_heal_fast(s->ox[p], s->oy[p] + POP_YOFF, s->ow[p], s->oh[p]); + s->valid[p] = s->ovalid[p] = 0; +} + +void pop_char_set_render_dx(uint8_t who, int dx) __banked +{ + pop_cd[who].render_dx = dx; +} + +/* Брызги урона (draw_hurt_splash, seg006:2003) — ОДИН порт на всех: у Кида + * это image 218 своего chtab_2, у соперника image 1 его chtab_5 (у + * add_midtable аргумент `obj_id + 1`, а get_image вычитает единицу обратно + * — тот же off-by-one, что расписан в шапке pop_pack_guard.py). Подъём: + * `((charid == kid) << 2) + 11`, то есть 15 у Кида и 11 у остальных. + * + * Рисуются МЕЖДУ персонажем и клинком: draw_kid/draw_guard кладут их в + * objtable сразу после персонажа и до клинка (seg008:22A2/22C9), а порядок + * записей и есть порядок рисования. */ +static void cd_splash(pop_cdraw_t *s, uint8_t who, const atlas_t *pages, + int fp_x, int obj_y) +{ + const atlas_t *ap; + uint8_t aidx; + const uint8_t *img; + uint16_t w, h; + int qx = fp_x, qy = obj_y; + int fw = (Char.direction < 0) ? -5 : 5; /* obj_dx_forward(5) */ + + if (Char.frame == 178) return; /* chomped: брызг нет */ + if (who == POP_CH_KID) { + if ((KID_SPLASH_IMAGE >> 3) >= kid_npages) return; /* страница не загружена */ + ap = &pages[KID_SPLASH_IMAGE >> 3]; aidx = KID_SPLASH_IMAGE & 7; + } else { + ap = &pages[0]; aidx = 1; + } + if (Char.frame == 185 || /* мёртв */ + (Char.frame >= 106 && Char.frame < 111)) { /* падение+посадка */ + qy += 4; + } else if (Char.frame == 177) { /* напоролся на пики */ + fw = -fw; /* obj_dx_forward(-5) */ + } else { + qy -= (who == POP_CH_KID) ? 15 : 11; + } + qx = (qx + fw) * 8 / 7; /* calc_screen_x_coord */ + img = (const uint8_t *)atlas_image(ap, aidx); + gfx_w0_map(ap->page); + w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); + h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); + if (w && h) { + int top = qy - (int)h + 1; + uint8_t flip = (uint8_t)(Char.direction >= 0); + int skip = 0, rows = (int)h; /* поле комнаты 0..192 */ + uint8_t dp; + if (flip) qx -= (int)w; + if (top < 0) { skip = -top; rows -= skip; top = 0; } + if (top + rows > 192) rows = 192 - top; + if (rows <= 0) return; + gfx_set_bank(GFX_BANK_SPRITE); + if (pop_onscreen_cols(qx, top + POP_YOFF, w, (uint16_t)rows)) + gfx_blit_cols_part_noclip(qx, top + POP_YOFF, img, flip, + (uint8_t)skip, (uint8_t)rows); + else + gfx_blit_cols_part(qx, top + POP_YOFF, img, flip, skip, rows); + gfx_set_bank(GFX_BANK_NORMAL); + dp = gfx_get_draw_page() & 1; + cd_overlay_add(s, dp, qx, top, w, (uint16_t)rows); + cd_clip_add(s, qx, top, w, (uint16_t)rows); + } +} + +void pop_char_draw(uint8_t who) __banked +{ + pop_cdraw_t *s = &pop_cd[who]; + const pop_char_t *ch; + const kframe *fr; + const atlas_t *pages; + const uint8_t *img; + uint8_t npages, page, idx, dp, flip, hurt; + uint16_t w, h, vis_w; + int obj_x, obj_y, fp_x, fwd, top, bx, skip, rows, ct, cr; + + s->cw = 0; /* окно fore-клипа: пусто, пока не нарисовали */ + s->fpw = 0; /* габарит кадра: нечего перекрывать */ + + /* Окно Char (loadkid/loadshad) — как в оригинале: clip_char, футпринт и + * брызги работают с АКТИВНЫМ персонажем, а к этому моменту в Char лежит + * тот, кто тикал последним. Выгружать назад нечего: отрисовка только + * читает. */ + if (who == POP_CH_KID) { + pop_loadkid(); + ch = &Kid; fr = &kid_frame; pages = kidp; npages = kid_npages; + hurt = pop_kid_hurt; + } else { + pop_loadshad(); + ch = &Guard; fr = &pop_gframe; pages = gp; + npages = g_ok ? GUARD_PAGES : 0; + hurt = pop_guard_hurt; + if (Guard.charid == 0) return; /* соперника на сцене нет */ + } + if (fr->image == 255) return; /* пустой кадр */ + + /* load_frame_to_obj (seg008:1728): НИЗ спрайта на obj_y, левый край на + * obj_x. +render_dx — straddle-смещение (∓140), когда комната персонажа + * не совпадает с отрисованной: спрайт за кромкой. Коллизия НЕ смещается + * — она в кадре своей комнаты, смещение чисто рендерное. */ + fwd = (int)ch->x + ((ch->direction < 0) ? -fr->dx : fr->dx) + s->render_dx; + obj_x = (fwd << 1) - 116; + obj_y = (int)ch->y + fr->dy; + if ((int8_t)(fr->flags ^ ch->direction) >= 0) + obj_x++; /* even/odd pixel (в ЛОГИЧЕСКОЙ X) */ + fp_x = obj_x; /* ЛОГИЧЕСКАЯ X — для футпринта */ + /* Позицию кадра публикуем СРАЗУ: её читает физика (check_spike_below — + * char_x_left активного персонажа), и она обязана обновиться даже в + * кадре, который рисовать нечем. Размер (fpw) — ниже, по факту кадра: + * он же служит признаком «есть что перекрывать» для fore-прохода. */ + s->fpx = fp_x; s->fpy = obj_y; + /* seg008 calc_screen_x_coord: логическая X (280 шир = 28px/тайл) -> + * экран (320 шир = 32px/тайл), ×320/280 = ×8/7. БЕЗ этого персонаж + * уезжает влево от тайлов тем сильнее, чем правее. */ + obj_x = obj_x * 8 / 7; + + page = (uint8_t)(fr->image >> 3); + idx = (uint8_t)(fr->image & 7); + if (page >= npages) return; /* страница набора не загружена */ + img = (const uint8_t *)atlas_image(&pages[page], idx); + gfx_w0_map(pages[page].page); + w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); + h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); + dp = gfx_get_draw_page() & 1; + if (w && h) { + /* Спрайты нарисованы ЛИЦОМ ВЛЕВО (как в оригинале); seg008:864 — + * флип, когда персонаж смотрит ВПРАВО, и тогда xpos -= image->w + * (draw_mid). Флип column-major бесплатен, но зеркалит ВНУТРИ + * [x,x+w), поэтому при флипе подаём x−w. */ + flip = (uint8_t)(ch->direction >= 0); + bx = obj_x; + if (flip) bx -= (int)w; + top = obj_y - (int)h + 1; + vis_w = w; + skip = 0; + /* clip_char (seg006:1231/1749): тайл над головой — стена/пол → всё + * выше линии полосы своего ряда НЕ рисуется, иначе персонаж + * просвечивает сквозь щели кладки при спуске/висе. */ + ct = pop_clip_char_top(fp_x, obj_y, w, h); + if (ct > top) { + skip = ct - top; + if (skip > (int)h) skip = (int)h; /* обрезан целиком */ + top = ct; + } + /* Вторая половина clip_char: правый косяк двери уровня. Дверь + * рисуется ПОД персонажем (вся в backtable), поэтому спрятать + * поднимающегося по лестнице можно только срезав спрайт. */ + cr = pop_clip_char_right(); + if (cr) { + int avail = cr - bx; + if (avail <= 0) vis_w = 0; /* весь спрайт за косяком */ + else if (avail < (int)w) vis_w = (uint16_t)avail; + } + /* Поле комнаты — 192 строки (reset_obj_clip, seg006:0507: + * obj_clip_top = 0, obj_clip_bottom = 192). Персонаж, вышедший за + * него (скелет, падающий в пропасть), обязан обрезаться: ниже поля + * идёт полоса HP, и без клипа спрайт лез прямо на неё. */ + rows = (int)h - skip; + if (top < 0) { skip += -top; rows += top; top = 0; } + if (top + rows > 192) rows = 192 - top; + gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */ + if (rows > 0 && vis_w) { /* +YOFF: центрирование */ + /* noclip-ядро ширину не ограничивает, поэтому при обрезке справа + * идём общим путём: это редкие кадры (подъём в двери уровня раз + * за уровень), цена подготовки там не видна. */ + if (vis_w == w && pop_onscreen_cols(bx, top + POP_YOFF, w, (uint16_t)rows)) + gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, + (uint8_t)skip, (uint8_t)rows); + else + gfx_blit_cols_part_w(bx, top + POP_YOFF, img, flip, skip, rows, + (uint8_t)(vis_w == w ? 0 : vis_w)); + } else rows = 0; + gfx_set_bank(GFX_BANK_NORMAL); + /* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над + * срезом (мусор/дыра в кладке на второй странице дабл-буфера). */ + s->x[dp] = bx; s->y[dp] = top; + s->w[dp] = vis_w; s->h[dp] = (uint16_t)rows; + s->valid[dp] = (uint8_t)(rows != 0); + if (rows) cd_clip_add(s, bx, top, vis_w, (uint16_t)rows); + s->fpw = w; s->fph = h; /* габарит КАДРА (не обрезанный): */ + /* по нему считается футпринт */ + } + if (hurt) cd_splash(s, who, pages, fp_x, obj_y); + /* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх + * персонажа — со своим прямоугольником heal. */ + { + int sx, sy; uint16_t sw, sh; + if (pop_sword_draw(ch, fr, obj_x, obj_y, &sx, &sy, &sw, &sh)) { + cd_overlay_add(s, dp, sx, sy, sw, sh); + cd_clip_add(s, sx, sy, sw, sh); + } + } + gfx_w0_unmap(); +} + +void pop_char_fore(uint8_t who) __banked +{ + pop_cdraw_t *s = &pop_cd[who]; + /* Окно fore-клипа — одно на всех, и ставит его КАЖДЫЙ, кто рисует + * персонажа. Пока соперник рисовался строго ДО Кида, к моменту прохода + * в окне оказывался Кид и всё сходилось само; как только порядок стал + * переменным (кто ниже — тот поверх), при «соперник поверх» окно + * оставалось ЕГО, и fore-проход Кида отсекался целиком — Кид рисовался + * поверх передних столбов. Поэтому окно ставится здесь, а не держится + * «по счастливому порядку вызовов». */ + pop_fore_set_clip(s->cx, s->cy + POP_YOFF, (int)s->cw, (int)s->ch); + if (s->fpw) + pop_fore_over_char(who == POP_CH_KID ? &Kid : &Guard, + s->fpx, s->fpy, s->fpw, s->fph); +} + +/* ---- Индикаторы HP (порт draw_kid_hp / draw_guard_hp, seg000:1416) --- * + * Оригинал рисует их в статус-полосе на y = 194 (ниже поля тайлов, внутри + * тех же 200 строк экрана PoP): HP Кида слева направо шагом 7, HP стража + * справа налево от x = 314. Пустые деления у Кида — свой спрайт (chtab_2 + * id 217); у стража оригинал печатает тот же спрайт ЧЁРНЫМ силуэтом, а мы + * просто не печатаем — полоса и так на чёрном фоне. + * + * Спрайты Кида (216/217) лежат в его же атласах, поэтому блитим через + * pop_kid_img_blit; спрайт стража — image 0 его chtab (наша страница 0). */ +#define HP_Y 194 +#define HP_STEP 7 +#define HP_KID_FULL 216 +#define HP_KID_EMPTY 217 +#define HP_MAXDRAW 10 /* больше в полосу всё равно не влезает */ + +/* Полоса перерисовывается ТОЛЬКО когда числа изменились — и тогда на ОБЕИХ + * страницах дабл-буфера (иначе на второй останется старое значение и его + * будет видно как мерцание через кадр). Отсюда счётчик страниц, а не флаг. + * + * Снимки прошлых значений пишем ДО сравнения: `if (n != s) s = n;` SDCC z80 + * собирает неверно — SUB затирает A, и в s уезжает разность (memory + * sdcc_z80_cmp_store_a_bug). */ +static uint8_t hp_todo; /* сколько страниц ещё обновить */ +static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev; + +/* Заставить перерисовать полосу на обеих страницах: звать при входе в + * комнату (фон перерисован целиком и стёр её) и при старте. */ +void pop_hp_invalidate(void) __banked { hp_todo = 2; } + +void pop_hp_draw(void) __banked +{ + uint8_t i, n; + uint8_t gd = (uint8_t)(Guard.charid ? guardhp_curr : 0); + uint8_t gm = (uint8_t)(Guard.charid ? guardhp_max : 0); + + { /* Изменилось хоть одно число — обновить обе страницы. */ + uint8_t k = hitp_curr, km = hitp_max; + uint8_t changed = (uint8_t)(k != hp_kid_prev || km != hp_kidmax_prev || + gd != hp_gd_prev || gm != hp_gdmax_prev); + hp_kid_prev = k; hp_kidmax_prev = km; + hp_gd_prev = gd; hp_gdmax_prev = gm; + if (changed) hp_todo = 2; + } + if (hp_todo == 0) return; + hp_todo--; + + /* Стереть прошлую полосу: фон под ней — чёрный борт. */ + gfx_set_bank(GFX_BANK_NORMAL); + setfillstyle(SOLID_FILL, BLACK); + bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6); + + /* Кид: сначала пустые деления, поверх — полные (порядок оригинала). */ + n = (uint8_t)(hitp_max > HP_MAXDRAW ? HP_MAXDRAW : hitp_max); + for (i = hitp_curr; i < n; i++) + pop_kid_img_blit(HP_KID_EMPTY, (int)i * HP_STEP, HP_Y + POP_YOFF); + for (i = 0; i < hitp_curr && i < HP_MAXDRAW; i++) + pop_kid_img_blit(HP_KID_FULL, (int)i * HP_STEP, HP_Y + POP_YOFF); + + /* Страж: только полные деления, справа налево. Скелет полосы не имеет. */ + if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON && gd) { + const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0); + uint16_t w, h; + gfx_w0_map(gp[0].page); + w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); + h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); + if (w && h) { + /* Атлас стража тоже column-major — блит колоночный. */ + n = (uint8_t)(gd > HP_MAXDRAW ? HP_MAXDRAW : gd); + for (i = 0; i < n; i++) + gfx_blit_cols_part_noclip(314 - (int)i * HP_STEP, + HP_Y + POP_YOFF, img, 0, 0, 0); + } + gfx_w0_unmap(); + } +} diff --git a/applications/PoP/roomtest/pop_cdraw.h b/applications/PoP/roomtest/pop_cdraw.h new file mode 100644 index 0000000..d433c6d --- /dev/null +++ b/applications/PoP/roomtest/pop_cdraw.h @@ -0,0 +1,93 @@ +/* + * pop_cdraw.h — ОТРИСОВКА ПЕРСОНАЖА, одна на всех Char. + * + * В оригинале это два входа с ИДЕНТИЧНЫМ телом (seg008:22F0/2324): + * + * add_kid_to_objtable add_guard_to_objtable + * loadkid() loadshad() + * load_fram_det_col() load_fram_det_col() + * load_frame_to_obj() load_frame_to_obj() + * stuck_lower() stuck_lower() + * set_char_collision() set_char_collision() + * set_objtile_at_char() set_objtile_at_char() + * redraw_at_char() redraw_at_char() + * redraw_at_char2() redraw_at_char2() + * clip_char() clip_char() + * add_objtable(0) add_objtable(1 тень / 2 страж) + * + * — различаются ТОЛЬКО окном (loadkid/loadshad), набором спрайтов и типом + * объекта. У нас до DRAW-CHAR это были два независимых куска кода + * (kid_draw в резиденте и pop_guard_draw в банке), и расхождение уже стоило + * бага: падающий скелет рисовался поверх верхней грани пола соседней + * колонки, потому что порт redraw_at_char2 был только у Кида. Тем же путём + * пошли бы тень (ур. 4/5/6/12), визирь (13) и толстяк (12). + * + * Поэтому здесь ОДИН набор функций, а различия сведены к слоту: + * POP_CH_KID — Кид: окно loadkid, атласы chtab_2 (kidp), кадр kid_frame; + * POP_CH_OPP — соперник: окно loadshad, атласы chtab_5 (страж/скелет), + * кадр pop_gframe. + * + * Порядок в кадре (задаёт главный цикл, как draw_people): + * pop_char_heal(who) — стереть прошлый кадр ЭТОЙ страницы дабл-буфера; + * pop_char_draw(who) — спрайт + брызги урона + клинок; + * pop_char_fore(who) — передние грани и оверлеи тайлов ПОВЕРХ него. + */ +#ifndef POP_CDRAW_H +#define POP_CDRAW_H + +#include + +#define POP_CH_KID 0 +#define POP_CH_OPP 1 +#define POP_CH_N 2 + +/* Состояние отрисовки одного слота. Публичное, потому что метрики кадра + * (fp*) читает физика из ДРУГОГО банка (char_width_half в + * set_char_collision, порядок «кто поверх кого» в pop_bg): данные лежат в + * _DATA (W2) и видны всем без трамплина, а вызов аксессора из банка стоил + * бы дороже самого чтения. */ +typedef struct { + /* прямоугольник спрайта по СТРАНИЦАМ дабл-буфера: у каждой своя + * видео-ОЗУ и своя ОЗУ-копия, поэтому heal обязан стирать спрайт именно + * той страницы, в которую сейчас рисуем */ + int x[2], y[2]; + uint16_t w[2], h[2]; + uint8_t valid[2]; + /* НАКЛАДНЫЕ спрайты (клинок + брызги урона) — свой прямоугольник, не + * объединение с персонажем: объединение сильно больше суммы двух + * (клинок уходит вперёд-вверх), а heal стоит ровно по площади */ + int ox[2], oy[2]; + uint16_t ow[2], oh[2]; + uint8_t ovalid[2]; + /* габарит кадра для fore-прохода: fpx — ЛОГИЧЕСКАЯ X (до ×8/7), + * fpy — низ спрайта; fpw == 0 — в этом кадре рисовать было нечего */ + int fpx, fpy; + uint16_t fpw, fph; + /* окно fore-клипа слота (объединение «спрайт + накладные»), КОМНАТНЫЙ y */ + int cx, cy; + uint16_t cw, ch; + /* straddle: рендерное смещение по ЛОГИЧЕСКОЙ X, когда комната персонажа + * не совпадает с отрисованной (порт xpos_in_drawn_room) */ + int render_dx; +} pop_cdraw_t; + +extern pop_cdraw_t pop_cd[POP_CH_N]; + +/* Стереть прошлый кадр персонажа (спрайт + накладные) на ТЕКУЩЕЙ странице. */ +void pop_char_heal(uint8_t who) __banked; + +/* Нарисовать текущий кадр: спрайт, брызги урона (по флагу pop_kid_hurt / + * pop_guard_hurt) и клинок. Загружает окно Char сам — clip_char и футпринт + * работают с АКТИВНЫМ персонажем, как в оригинале. */ +void pop_char_draw(uint8_t who) __banked; + +/* Передние грани и оверлеи тайлов ПОВЕРХ персонажа: ставит окно fore-клипа + * слота и зовёт общий проход pop_fore_over_char. Отдельным вызовом, а не + * хвостом pop_char_draw, потому что порядок в кадре задаёт главный цикл: + * между спрайтом и его fore-слоем успевает нарисоваться падающая плита. */ +void pop_char_fore(uint8_t who) __banked; + +/* Straddle-смещение спрайта (см. render_dx). */ +void pop_char_set_render_dx(uint8_t who, int dx) __banked; + +#endif diff --git a/applications/PoP/roomtest/pop_draw.c b/applications/PoP/roomtest/pop_draw.c index e78802b..0f34f79 100644 --- a/applications/PoP/roomtest/pop_draw.c +++ b/applications/PoP/roomtest/pop_draw.c @@ -7,13 +7,13 @@ * CLIP-1 (2026-08-01), SDCC 4.5: `static inline` тут обходится втридорога — * компилятор И встраивает тело в каждый вызов (181 Б на место), И оставляет * отдельную мёртвую копию в каждом TU. У стража это раздуло - * `pop_guard_heal` с ~60 до 663 Б. Пяти местам вызова это стоило бы ~1.4 КБ + * тогдашний `pop_guard_heal` с ~60 до 663 Б. Пяти местам вызова это стоило бы ~1.4 КБ * ради экономии ~50 тактов на вызов — при том что BANK2 занят на 86 %. * * Резидент W1 (_CODE), а не банк: сюда зовут из ДВУХ разных банков - * (pop_bg.c → BANK2, pop_gdraw.c → BANK4) и из резидента (pop_kid.c). W1 - * замаплено всегда, поэтому из банка это обычный `call` без трамплина — - * тот же приём, что у pop_sword_draw. + * (pop_bg.c → BANK2, pop_cdraw.c → BANK4). W1 замаплено всегда, поэтому из + * банка это обычный `call` без трамплина — тот же приём, что у + * pop_sword_draw. */ #include #include diff --git a/applications/PoP/roomtest/pop_gdraw.c b/applications/PoP/roomtest/pop_gdraw.c deleted file mode 100644 index 5c8a128..0000000 --- a/applications/PoP/roomtest/pop_gdraw.c +++ /dev/null @@ -1,371 +0,0 @@ -/* - * pop_gdraw.c — ОТРИСОВКА стража. Резидент окна W3 (--w3 pop_gdraw.c). - * - * Почему отдельным файлом, а не внутри pop_guard.c: --w3 забирает МОДУЛЬ - * целиком (код+rodata в 0xC000), а состояние стража и его логика обязаны - * остаться в W1/W2 — из банка (guards.c) резидент W3 недостижим ни прямо, - * ни транзитивно (docs/layout_plan_v2.md §2 R2). Значит граница проходит - * ровно между «что рисует» (зовёт только главный цикл → резидент) и «что - * знает» (зовёт и банк → W1/W2). - * - * Писучие статики резидента (--dataseg не трогаем) остаются в обычном - * _DATA, то есть в W2 — сюда переезжает КОД, не данные. - */ -#include -#include -#include -#include "pop_guard.h" -#include "pop_kid.h" /* pop_sword_draw — клинок общий с Кидом */ -#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */ -#include "_pop_draw.h" /* pop_onscreen_cols / pop_heal_fast */ -#include "pop_map.h" /* hitp_curr/hitp_max — HP Кида */ -#include "pop_level.h" /* тип соперника уровня (набор спрайтов) */ -#include "kid_data.h" -#include "pop_guard_pal.h" /* таблица палитр стражей (сгенерена упаковщиком) */ - -/* Цвет стража (set_chtab_palette, seg003:257). Оригинал держит ОДИН набор - * спрайтов и подменяет 16 цветов палитры chtab_5 по level.guards_color — - * так на уровне 2 стражи комнат 4/7/11/15/24 разного цвета (1/3/1/1/6). - * Полоса HP стража рисуется тем же атласом, поэтому меняется заодно. - * color = 0 — палитру не трогаем (оригинал в этом случае set_chtab_palette - * не зовёт: тип уровня не «обычный страж»). - * Грузим В ОБЕ палитры: у страниц дабл-буфера они раздельные. */ -void pop_guard_set_palette(uint8_t color) __banked -{ - /* Таблица — rodata ЭТОГО модуля, то есть страница банка (0xC000+), а - * gfx_pal_load отдаёт указатель в BIOS ($A4 через rst #0x08). BIOS - * читает только #4000-#BFFF (см. корневой CLAUDE.md), поэтому копируем - * запись в локальный буфер — стек гарантированно в W2. Без копии BIOS - * забирал мусор с чужой страницы и страж становился невидимым. */ - uint8_t buf[64]; - const uint8_t *p; - uint8_t i; - if (color == 0 || color > POP_GUARD_NPAL) return; - p = pop_guard_pal[color - 1]; - for (i = 0; i < 64; i++) buf[i] = p[i]; - gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf); - gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf); -} - -/* ---- Атласы стража (chtab_5, база 750): g[id>>3], idx id&7 ---------- */ -#define GUARD_PAGES 5 -static atlas_t gp[GUARD_PAGES]; -static uint8_t g_ok; - -/* Прямоугольник последней отрисовки ПО СТРАНИЦЕ дабл-буфера (как у Kid: - * heal обязан стирать спрайт именно той страницы, в которую рисуем). */ -static int gx_l[2], gy_l[2]; -static uint16_t gw_l[2], gh_l[2]; -static uint8_t gvalid[2]; - -/* Клинок — ОТДЕЛЬНЫМ прямоугольником, не объединением со стражем: объединение - * почти вдвое больше суммы двух (клинок уходит вперёд-вверх), а heal стоит - * ровно по площади. Объединение считаем только для окна fore-клипа. */ -static int sx_l[2], sy_l[2]; -static uint16_t sw_l[2], sh_l[2]; -static uint8_t svalid[2]; - -/* Брызги удара — тоже свой прямоугольник, по той же причине: спрайт мелкий и - * уезжает вперёд-вверх от стража. Живут они ОДИН кадр, но стереть их обязана - * та же страница, в которую рисовали (иначе остаток виден через кадр). */ -static int qx_l[2], qy_l[2]; -static uint16_t qw_l[2], qh_l[2]; -static uint8_t qvalid[2]; - -/* Ширина спрайта последнего кадра стража — нужна set_char_collision - * (char_width_half) при вычислении его ТАЙЛА ОБЪЕКТА. Не по страницам: - * это свойство кадра, а не буфера. Отставание на кадр допустимо — так же - * работает kid_fp_width() у Кида. */ -static uint16_t g_last_w; - -uint16_t pop_guard_fp_width(void) __banked { return g_last_w; } - -/* Набор спрайтов соперника выбирает ТИП УРОВНЯ (load_lev_spr, seg000:1092): - * tbl_guard_type = {0 guard, 1 fat, 2 skeleton, 3 vizier, 4 shadow, −1 нет}. - * У нас пока два набора: обычный страж (уровни 1-2) и скелет (уровень 3); - * у скелета и кадров меньше (28 против 34), поэтому пятую страницу он не - * грузит. Уровень БЕЗ соперников (type < 0) не грузит ничего. */ -int pop_guard_load(void) __banked -{ - static const char *const guard_names[GUARD_PAGES] = { - "GUARD\\g0.atl", "GUARD\\g1.atl", "GUARD\\g2.atl", - "GUARD\\g3.atl", "GUARD\\g4.atl" - }; - static const char *const skel_names[GUARD_PAGES] = { - "SKEL\\g0.atl", "SKEL\\g1.atl", "SKEL\\g2.atl", - "SKEL\\g3.atl", 0 - }; - const char *const *names; - int8_t type = pop_level_guard_type(pop_current_level); - uint8_t i; - if (type < 0) return 0; /* соперников на уровне нет */ - names = (type == 2) ? skel_names : guard_names; - for (i = 0; i < GUARD_PAGES; i++) { - if (names[i] == 0) break; - if (atlas_load(&gp[i], names[i]) != 0) return -1; - } - g_ok = 1; - /* Цвет: у скелета он СВОЙ (палитра из его набора), у стража подменяется - * из res10.bin по guards_color — это делает pop_guard_set_palette при - * входе в комнату (seg002:186: у не-guard curr_guard_color = 0). */ - if (type == 2) { - uint8_t buf[64], k; - for (k = 0; k < 64; k++) buf[k] = pop_skel_pal[k]; /* BIOS: буфер в W2 */ - gfx_pal_load(0, POP_GUARD_PAL_BASE, 16, buf); - gfx_pal_load(1, POP_GUARD_PAL_BASE, 16, buf); - } - return 0; -} - -void pop_guard_free(void) __banked -{ - uint8_t i; - if (!g_ok) return; - for (i = 0; i < GUARD_PAGES; i++) atlas_free(&gp[i]); - g_ok = 0; -} - -/* Стирание прошлого кадра стража и его клинка. pop_heal_fast — по той же - * причине, что в kid_heal: стираем ровно то, что рисовали, а рисовали в - * большинстве кадров noclip-ядром. Два прямоугольника, а не объединение - * (см. комментарий к sx_l выше). */ -void pop_guard_heal(void) __banked -{ - uint8_t p = gfx_get_draw_page() & 1; - if (gvalid[p]) pop_heal_fast(gx_l[p], gy_l[p] + POP_YOFF, gw_l[p], gh_l[p]); - if (svalid[p]) pop_heal_fast(sx_l[p], sy_l[p] + POP_YOFF, sw_l[p], sh_l[p]); - if (qvalid[p]) pop_heal_fast(qx_l[p], qy_l[p] + POP_YOFF, qw_l[p], qh_l[p]); - gvalid[p] = svalid[p] = qvalid[p] = 0; -} - -void pop_guard_draw(void) __banked -{ - int obj_x, obj_y, top, fwd, fp_x; - uint8_t page, idx, p, flip; - const uint8_t *img; - uint16_t w, h; - /* Окно fore-клипа — по ОБЪЕДИНЁННОМУ габариту «страж + клинок»; сам - * футпринт (pop_fore_over_char) считается по спрайту стража, поэтому - * w/h ниже НЕ трогаем. */ - int clip_x = 0, clip_y = 0; uint16_t clip_w = 0, clip_h = 0; - - if (!g_ok || Guard.charid == 0 || pop_gframe.image == 255) return; - /* load_frame_to_obj (seg008:1728) — та же математика, что у Kid. */ - fwd = (int)Guard.x + ((Guard.direction < 0) ? -pop_gframe.dx : pop_gframe.dx); - obj_x = (fwd << 1) - 116; - obj_y = (int)Guard.y + pop_gframe.dy; - if ((int8_t)(pop_gframe.flags ^ Guard.direction) >= 0) obj_x++; - fp_x = obj_x; /* ЛОГИЧЕСКАЯ X (до ×8/7) — футпринт */ - obj_x = obj_x * 8 / 7; /* calc_screen_x_coord */ - - page = (uint8_t)(pop_gframe.image >> 3); - idx = (uint8_t)(pop_gframe.image & 7); - if (page >= GUARD_PAGES) return; - img = (const uint8_t *)atlas_image(&gp[page], idx); - gfx_w0_map(gp[page].page); - w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); - h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); - if (w && h) { - int bx = obj_x; - /* Поле комнаты — 192 строки (reset_obj_clip, seg006:0507: - * obj_clip_top = 0, obj_clip_bottom = 192). Персонаж, вышедший за - * него (скелет, падающий в пропасть), обязан обрезаться: ниже поля - * идёт полоса HP, и без клипа спрайт лез прямо на неё. */ - int skip = 0, rows; - top = obj_y - (int)h + 1; - flip = (uint8_t)(Guard.direction >= 0); /* спрайты нарисованы влево */ - if (flip) bx -= (int)w; - rows = (int)h; - if (top < 0) { skip = -top; rows -= skip; top = 0; } - if (top + rows > 192) rows = 192 - top; - if (rows > 0) { - p = gfx_get_draw_page() & 1; - gfx_set_bank(GFX_BANK_SPRITE); - if (pop_onscreen_cols(bx, top + POP_YOFF, w, (uint16_t)rows)) - gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, - (uint8_t)skip, (uint8_t)rows); - else - gfx_blit_cols_part(bx, top + POP_YOFF, img, flip, skip, rows); - gfx_set_bank(GFX_BANK_NORMAL); - /* heal — ровно по НАРИСОВАННОМУ (обрезанному) прямоугольнику. */ - gx_l[p] = bx; gy_l[p] = top; gw_l[p] = w; gh_l[p] = (uint16_t)rows; - gvalid[p] = 1; - clip_x = bx; clip_y = top; clip_w = w; clip_h = (uint16_t)rows; - } - g_last_w = w; /* для char_width_half, см. выше */ - } - /* Брызги удара (draw_hurt_splash, seg006:16CE) — ровно здесь, между - * стражем и клинком: draw_guard кладёт их в objtable СРАЗУ после стража - * и ДО клинка (seg008:22C9), а порядок записей задаёт порядок рисования. - * - * Спрайт — chtab_5_guard `obj_id = 1`, то есть image 1 нашего атласа - * стража (страница 0, индекс 1): у add_midtable аргумент `obj_id + 1`, а - * get_image вычитает единицу обратно (тот же off-by-one, что расписан в - * шапке pop_pack_guard.py). У Кида это image 218 своего chtab. - * - * Смещения от кадра персонажа — по трём веткам оригинала; у стража - * подъём на 11 (у Кида 15: `((charid == kid) << 2) + 11`). */ - if (pop_guard_hurt && Guard.frame != 178 /* chomped: брызг нет */) { - int qx = fp_x, qy = obj_y; - const uint8_t *qimg = (const uint8_t *)atlas_image(&gp[0], 1); - uint16_t qw, qh; - int fw = (Guard.direction < 0) ? -5 : 5; /* obj_dx_forward(5) */ - if (Guard.frame == 185 || /* мёртв */ - (Guard.frame >= 106 && Guard.frame < 111)) { /* падение+посадка */ - qy += 4; - } else if (Guard.frame == 177) { /* напоролся на пики */ - fw = -fw; /* obj_dx_forward(-5) */ - } else { - qy -= 11; - } - qx = (qx + fw) * 8 / 7; /* calc_screen_x_coord */ - gfx_w0_map(gp[0].page); - qw = (uint16_t)(qimg[0] | ((uint16_t)qimg[1] << 8)); - qh = (uint16_t)(qimg[2] | ((uint16_t)qimg[3] << 8)); - if (qw && qh) { - int qtop = qy - (int)qh + 1; - uint8_t qflip = (uint8_t)(Guard.direction >= 0); - int qskip = 0, qrows = (int)qh; /* клип полем 192, как у спрайта */ - if (qflip) qx -= (int)qw; - if (qtop < 0) { qskip = -qtop; qrows -= qskip; qtop = 0; } - if (qtop + qrows > 192) qrows = 192 - qtop; - if (qrows <= 0) goto splash_done; - qh = (uint16_t)qrows; - gfx_set_bank(GFX_BANK_SPRITE); - if (pop_onscreen_cols(qx, qtop + POP_YOFF, qw, qh)) - gfx_blit_cols_part_noclip(qx, qtop + POP_YOFF, qimg, qflip, - (uint8_t)qskip, (uint8_t)qrows); - else - gfx_blit_cols_part(qx, qtop + POP_YOFF, qimg, qflip, qskip, qrows); - gfx_set_bank(GFX_BANK_NORMAL); - p = gfx_get_draw_page() & 1; - qx_l[p] = qx; qy_l[p] = qtop; qw_l[p] = qw; qh_l[p] = qh; - qvalid[p] = 1; - if (!clip_w) { - clip_x = qx; clip_y = qtop; clip_w = qw; clip_h = qh; - } else { - int x1 = clip_x + (int)clip_w, y1 = clip_y + (int)clip_h; - if (qx < clip_x) clip_x = qx; - if (qtop < clip_y) clip_y = qtop; - if (qx + (int)qw > x1) x1 = qx + (int)qw; - if (qtop + (int)qh > y1) y1 = qtop + (int)qh; - clip_w = (uint16_t)(x1 - clip_x); - clip_h = (uint16_t)(y1 - clip_y); - } - } -splash_done: ; - } - /* Клинок стража — тот же chtab_0, что у Кида (add_sword_to_objtable, - * seg006:1798): у ЖИВОГО стража меч в руке всегда. Свой прямоугольник - * heal; окно fore-клипа — объединение (без него клинок полез бы поверх - * переднего столба). */ - { - int sx, sy; uint16_t sw, sh; - if (pop_sword_draw(&Guard, &pop_gframe, obj_x, obj_y, - &sx, &sy, &sw, &sh)) { - p = gfx_get_draw_page() & 1; - sx_l[p] = sx; sy_l[p] = sy; sw_l[p] = sw; sh_l[p] = sh; - svalid[p] = 1; - if (!clip_w) { - clip_x = sx; clip_y = sy; clip_w = sw; clip_h = sh; - } else { - int x1 = clip_x + (int)clip_w, y1 = clip_y + (int)clip_h; - if (sx < clip_x) clip_x = sx; - if (sy < clip_y) clip_y = sy; - if (sx + (int)sw > x1) x1 = sx + (int)sw; - if (sy + (int)sh > y1) y1 = sy + (int)sh; - clip_w = (uint16_t)(x1 - clip_x); - clip_h = (uint16_t)(y1 - clip_y); - } - } - } - gfx_w0_unmap(); - /* Окно fore-слоя = прямоугольник стража с клинком (см. pop_bg.c, - * fclip_on): без него проход возвращал ЦЕЛЫЕ тайлы — 432 К тактов. */ - pop_fore_set_clip(clip_x, clip_y + POP_YOFF, (int)clip_w, (int)clip_h); - /* Передние грани тайлов ПОВЕРХ стража: в оригинале персонаж — запись - * midtable, а foretable рисуется после всех midtable-записей, поэтому - * колонна/кромка пола перекрывает ЛЮБОГО персонажа, не только Kid - * (seg008:690 draw_tile_fore). Без этого страж виден сквозь передний - * столб. */ - if (w && h) - pop_fore_over_char(fp_x, obj_y, w, h, Guard.direction, Guard.sword, - clip_x, clip_y + POP_YOFF, clip_w, clip_h, - Guard.action, Guard.curr_col, Guard.curr_row); -} - -/* ---- Индикаторы HP (порт draw_kid_hp / draw_guard_hp, seg000:1416) --- * - * Оригинал рисует их в статус-полосе на y = 194 (ниже поля тайлов, внутри - * тех же 200 строк экрана PoP): HP Кида слева направо шагом 7, HP стража - * справа налево от x = 314. Пустые деления у Кида — свой спрайт (chtab_2 - * id 217); у стража оригинал печатает тот же спрайт ЧЁРНЫМ силуэтом, а мы - * просто не печатаем — полоса и так на чёрном фоне. - * - * Спрайты Кида (216/217) лежат в его же атласах, поэтому блитим через - * pop_kid_img_blit; спрайт стража — image 0 его chtab (наша страница 0). */ -#define HP_Y 194 -#define HP_STEP 7 -#define HP_KID_FULL 216 -#define HP_KID_EMPTY 217 -#define HP_MAXDRAW 10 /* больше в полосу всё равно не влезает */ - -/* Полоса перерисовывается ТОЛЬКО когда числа изменились — и тогда на ОБЕИХ - * страницах дабл-буфера (иначе на второй останется старое значение и его - * будет видно как мерцание через кадр). Отсюда счётчик страниц, а не флаг. - * - * Снимки прошлых значений пишем ДО сравнения: `if (n != s) s = n;` SDCC z80 - * собирает неверно — SUB затирает A, и в s уезжает разность (memory - * sdcc_z80_cmp_store_a_bug). */ -static uint8_t hp_todo; /* сколько страниц ещё обновить */ -static uint8_t hp_kid_prev, hp_kidmax_prev, hp_gd_prev, hp_gdmax_prev; - -/* Заставить перерисовать полосу на обеих страницах: звать при входе в - * комнату (фон перерисован целиком и стёр её) и при старте. */ -void pop_hp_invalidate(void) __banked { hp_todo = 2; } - -void pop_hp_draw(void) __banked -{ - uint8_t i, n; - uint8_t gd = (uint8_t)(Guard.charid ? guardhp_curr : 0); - uint8_t gm = (uint8_t)(Guard.charid ? guardhp_max : 0); - - { /* Изменилось хоть одно число — обновить обе страницы. */ - uint8_t k = hitp_curr, km = hitp_max; - uint8_t changed = (uint8_t)(k != hp_kid_prev || km != hp_kidmax_prev || - gd != hp_gd_prev || gm != hp_gdmax_prev); - hp_kid_prev = k; hp_kidmax_prev = km; - hp_gd_prev = gd; hp_gdmax_prev = gm; - if (changed) hp_todo = 2; - } - if (hp_todo == 0) return; - hp_todo--; - - /* Стереть прошлую полосу: фон под ней — чёрный борт. */ - gfx_set_bank(GFX_BANK_NORMAL); - setfillstyle(SOLID_FILL, BLACK); - bar(0, HP_Y + POP_YOFF, 319, HP_Y + POP_YOFF + 6); - - /* Кид: сначала пустые деления, поверх — полные (порядок оригинала). */ - n = (uint8_t)(hitp_max > HP_MAXDRAW ? HP_MAXDRAW : hitp_max); - for (i = hitp_curr; i < n; i++) - pop_kid_img_blit(HP_KID_EMPTY, (int)i * HP_STEP, HP_Y + POP_YOFF); - for (i = 0; i < hitp_curr && i < HP_MAXDRAW; i++) - pop_kid_img_blit(HP_KID_FULL, (int)i * HP_STEP, HP_Y + POP_YOFF); - - /* Страж: только полные деления, справа налево. Скелет полосы не имеет. */ - if (g_ok && Guard.charid != 0 && Guard.charid != CHARID_4_SKELETON && gd) { - const uint8_t *img = (const uint8_t *)atlas_image(&gp[0], 0); - uint16_t w, h; - gfx_w0_map(gp[0].page); - w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); - h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); - if (w && h) { - /* Атлас стража тоже column-major — блит колоночный. */ - n = (uint8_t)(gd > HP_MAXDRAW ? HP_MAXDRAW : gd); - for (i = 0; i < n; i++) - gfx_blit_cols_part_noclip(314 - (int)i * HP_STEP, - HP_Y + POP_YOFF, img, 0, 0, 0); - } - gfx_w0_unmap(); - } -} diff --git a/applications/PoP/roomtest/pop_guard.c b/applications/PoP/roomtest/pop_guard.c index e767e3d..2e3557d 100644 --- a/applications/PoP/roomtest/pop_guard.c +++ b/applications/PoP/roomtest/pop_guard.c @@ -3,7 +3,7 @@ * * Здесь только то, что должно оставаться достижимым из банка (guards.c): * поля Guard, HP, вход в комнату, чит-убийство, чтение кадра из таблицы. - * ОТРИСОВКА — в pop_gdraw.c, резиденте W3 (её зовёт лишь главный цикл). + * ОТРИСОВКА — в pop_cdraw.c (банк 4), общая на всех Char. */ #include #include "pop_guard.h" diff --git a/applications/PoP/roomtest/pop_guard.h b/applications/PoP/roomtest/pop_guard.h index 9e0af53..dcb31c6 100644 --- a/applications/PoP/roomtest/pop_guard.h +++ b/applications/PoP/roomtest/pop_guard.h @@ -7,7 +7,7 @@ * линкуются В СТРАНИЦУ БАНКА и снаружи не читаются, поэтому банк — * только код; * - ЛОГИКА (ИИ, боёвка) — в guards.c, собираемом как --bank 1=guards.c; - * - ОТРИСОВКА стража — в pop_gdraw.c, РЕЗИДЕНТЕ W3 (--w3): её зовёт + * - ОТРИСОВКА стража — в pop_cdraw.c (банк 4), общая на всех Char: её зовёт * только главный цикл, а из банка резидент недостижим. Так код * отрисовки не занимает дефицитные W1/W2 (куча). * @@ -90,7 +90,7 @@ extern int8_t guardhp_delta; /* <0 = в этом кадре отняли uint8_t pop_take_hp(uint8_t count); /* Взводится в кадре, когда СТРАЖУ убавили HP (guardhp_delta < 0 оригинала): - * по нему pop_guard_draw рисует «брызги» удара, как draw_guard в seg008:1654. + * по нему pop_char_draw рисует «брызги» удара, как draw_guard в seg008:1654. * Снимает главный цикл после отрисовки — стража могло не оказаться в * отрисованной комнате, и тогда флаг обязан просто пропасть. */ extern uint8_t pop_guard_hurt; @@ -172,9 +172,6 @@ void pop_guard_leave(void); * side — куда ушёл КИД: 0 left, 1 right, 2 up, 3 down. */ uint8_t pop_guard_follow(uint8_t new_room, uint8_t side); -/* Нарисовать текущий кадр стража на back-странице (банк GFX_BANK_SPRITE, - * как kid_draw) и стереть прошлый (heal). Реализация — pop_gdraw.c - * (резидент W3), поэтому звать ТОЛЬКО из главного цикла, не из банка. */ /* Полоса HP (Кид слева, страж справа) в статус-полосе. Рисует главный цикл * ПОСЛЕ персонажей — она ничего не перекрывает. */ void pop_hp_draw(void) __banked; @@ -187,13 +184,9 @@ void pop_hp_invalidate(void) __banked; * отрисовки комнаты, как это делает redraw_screen. */ void pop_guard_set_palette(uint8_t color) __banked; -void pop_guard_heal(void) __banked; -void pop_guard_draw(void) __banked; - -/* Ширина спрайта последнего нарисованного кадра стража (char_width_half - * в set_char_collision). Нужна для его ТАЙЛА ОБЪЕКТА — от него зависит - * порядок отрисовки относительно Кида. */ -uint16_t pop_guard_fp_width(void) __banked; +/* Отрисовка соперника — общая на всех Char: pop_char_draw(POP_CH_OPP) и + * компания, см. pop_cdraw.h. Метрики его последнего кадра (в т.ч. ширина + * спрайта для char_width_half) — pop_cd[POP_CH_OPP]. */ /* Убрать стража со сцены (смена комнаты / инициализация). */ void pop_guard_reset(void); diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index b1928a1..e2d4d19 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -31,10 +31,21 @@ int8_t knock; uint8_t pop_next_level; -/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ */ +/* ---- Атласы Kid (прямая адресация image>>3) ------------------------ * + * НЕ static: набор спрайтов слота POP_CH_KID общей отрисовки персонажей + * (pop_cdraw.c, банк 4). Отдан как есть, а не через аксессор: читается раз + * в кадр из другого банка, и вызов стоил бы дороже самого чтения. */ #define KID_MAXPAGES 28 -static atlas_t kidp[KID_MAXPAGES]; -static uint8_t kid_npages; +atlas_t kidp[KID_MAXPAGES]; +uint8_t kid_npages; + +/* Таблица «seq_id -> адрес в seqtbl» (original_seqtbl_offsets). Определена + * ЗДЕСЬ, а не в kid_data.h: заголовок включают почти все модули PoP, и + * `static const` в нём размножал таблицу мёртвой копией по каждому TU — + * 230 Б × 9 модулей, из них 460 Б в дефицитном резиденте W1/W2 (замер + * 2026-08-08). Наружу таблица не нужна: смену последовательности делают + * kid_set_seq / pop_char_set_seq. */ +static const uint16_t kid_seq_off[KID_NSEQ] = KID_SEQ_OFF_INIT; /* chtab_0 «меч в руке» (add_sword_to_objtable, seg006:1798): клинок — * ОТДЕЛЬНЫЙ спрайт поверх персонажа, один и тот же атлас у Кида и у стража. @@ -113,7 +124,7 @@ uint8_t pop_sword_draw(const pop_char_t *ch, const kframe *fr, /* Блит спрайта chtab_2 (атласы Kid) по ЕГО id, левым верхним углом в (x,y). * Нужен полосе HP: она рисует id 216/217 из тех же атласов, что и кадры - * Кида, а держать вторую копию раскладки в pop_gdraw ни к чему. */ + * Кида, а держать вторую копию раскладки в pop_cdraw ни к чему. */ void pop_kid_img_blit(uint8_t image, int x, int y) { uint8_t pg = (uint8_t)(image >> 3); /* раскладка атласов: 8 спрайтов/стр. */ @@ -129,7 +140,7 @@ void pop_kid_img_blit(uint8_t image, int x, int y) * ради бесплатного горизонтального флипа), поэтому блит обязан быть * колоночным. Row-major gfx_blit_noclip выдал бы марку транспонированной. * noclip БЕЗ проверки (CLIP-1): единственный вызывающий — полоса HP - * (pop_gdraw.c), у неё координаты фиксированные и заведомо на экране. */ + * (pop_cdraw.c), у неё координаты фиксированные и заведомо на экране. */ if (w && h) gfx_blit_cols_part_noclip(x, y, img, 0, 0, 0); gfx_w0_unmap(); } @@ -207,9 +218,11 @@ static uint8_t kdat_blk, kdat_page, kdat_ok; /* Текущий кадр (порт глобала cur_frame, seg006): 5 байт вместо таблицы. * Заполняется load_frame для АКТИВНОГО персонажа (Char), поэтому на * выходе из play_seq его надо сохранить в кэш своего персонажа: страж - * тикает ПОСЛЕ Кида, и без разделения kid_draw взял бы кадр стража. */ + * тикает ПОСЛЕ Кида, и без разделения отрисовка Кида взяла бы кадр стража. */ static kframe cur_frame; -static kframe kid_frame; /* кадр Kid для kid_draw */ +/* Кадр Кида для отрисовки — пара к pop_gframe соперника; читает общая + * отрисовка (pop_cdraw.c), поэтому не static. */ +kframe kid_frame; /* Char (seg006): АКТИВНЫЙ персонаж. Интерпретатор последовательностей в * оригинале работает не с Kid и не с Guard, а с этой копией, а вокруг @@ -349,7 +362,7 @@ static void load_frame(void) * персонажа, а не в save*-функциях. Раньше кэш писали pop_savekid/ * pop_saveshad, и любое окно Char БЕЗ play_seq (окна боёвки * check_sword_hurting / check_sword_hurt) записывало Киду кадр, который - * на самом деле принадлежал СТРАЖУ: kid_draw брал его image из атласа + * на самом деле принадлежал СТРАЖУ: отрисовка брала его image из атласа * Кида и рисовал произвольную позу. Теперь владелец один — тот, для * кого кадр реально загружен. */ if (Char.charid == CHARID_0_KID) kid_frame = cur_frame; @@ -519,292 +532,6 @@ void kid_tick(void) pop_kid_play(); } -/* Отрисовка текущего кадра: obj_x=(char_x+dx)*2-116, obj_y=char_y+dy - * (seg008.c:1728 load_frame_to_obj). НИЗ спрайта на obj_y, левый край - * на obj_x. Калибровка XOFF/YOFF — сверить с эталоном. - * - * Зеркалирование (K2a): спрайты Kid хранятся ЛИЦОМ ВЛЕВО (как в оригинале). - * seg008.c:864 — флип, когда персонаж смотрит ВПРАВО (direction==dir_0_right - * ==0), и тогда xpos -= image->w (draw_mid). Реализуем бесплатным - * горизонтальным флипом column-major спрайта (gfx_blit_cols flip=1): - * зеркало происходит ВНУТРИ [x,x+w), поэтому при флипе подаём x=obj_x-w, - * чтобы «hot-point» кадра остался на месте. - * - * Kid рисуется банком GFX_BANK_SPRITE (только видео-ОЗУ, НЕ теневая копия - * — 0xFF прозрачен), чтобы kid_heal() мог восстановить чистый фон из - * теневой копии (фон запёкся в неё при pop_room_draw). Прямоугольник - * последней отрисовки запоминается для heal. */ -#define KID_XOFF 0 -#define KID_YOFF 0 - -/* Прямоугольник последней отрисовки Kid — ПО СТРАНИЦЕ (0/1). У каждой - * графической страницы своя ОЗУ-копия/видео-ОЗУ, поэтому heal должен - * стирать пиксели Kid именно той страницы, в которую сейчас рисуем - * (при дабл-буфере страница чередуется — прошлый Kid на этой странице - * был 2 логических кадра назад). В однобуфере используется только [0]. */ -static int kid_lx[2], kid_ly[2]; -static uint16_t kid_lw[2], kid_lh[2]; -static uint8_t kid_lvalid[2]; - -/* ОТДЕЛЬНЫЙ прямоугольник накладных спрайтов (клинок chtab_0, брызги урона). - * Раньше он объединялся с прямоугольником персонажа — но объединение сильно - * больше суммы двух: клинок уходит вперёд-вверх, и общий габарит выходил - * почти вдвое шире и выше самого Kid, а heal стоит ровно по площади. - * Поэтому heal делаем двумя вызовами по своим прямоугольникам, а объединение - * оставляем ТОЛЬКО для окна fore-клипа (там это 4 сравнения, без рисования — - * зато клинок/брызги честно уходят под передние грани тайлов). */ -static int kid_ox[2], kid_oy[2]; -static uint16_t kid_ow[2], kid_oh[2]; -static uint8_t kid_ovalid[2]; - -/* Добавить накладной спрайт в overlay-прямоугольник страницы dp. Клинок и - * брызги в одном кадре встречаются редко и оба маленькие — их объединение - * дешевле третьего слота. */ -static void kid_overlay_add(uint8_t dp, int x, int y, uint16_t w, uint16_t h) -{ - if (!kid_ovalid[dp]) { - kid_ox[dp] = x; kid_oy[dp] = y; kid_ow[dp] = w; kid_oh[dp] = h; - kid_ovalid[dp] = 1; - return; - } - { - int x0 = kid_ox[dp], y0 = kid_oy[dp]; - int x1 = x0 + (int)kid_ow[dp], y1 = y0 + (int)kid_oh[dp]; - if (x < x0) x0 = x; - if (y < y0) y0 = y; - if (x + (int)w > x1) x1 = x + (int)w; - if (y + (int)h > y1) y1 = y + (int)h; - kid_ox[dp] = x0; kid_oy[dp] = y0; - kid_ow[dp] = (uint16_t)(x1 - x0); kid_oh[dp] = (uint16_t)(y1 - y0); - } -} - -/* Окно fore-клипа = объединение «персонаж + накладные». Считается по - * прямоугольникам, ничего не рисует. */ -static void kid_fore_clip(uint8_t dp) -{ - int x0, y0, x1, y1; - if (!kid_lvalid[dp] && !kid_ovalid[dp]) { pop_fore_set_clip(0, 0, 0, 0); return; } - if (!kid_ovalid[dp]) { - pop_fore_set_clip(kid_lx[dp], kid_ly[dp] + POP_YOFF, - (int)kid_lw[dp], (int)kid_lh[dp]); - return; - } - if (!kid_lvalid[dp]) { - pop_fore_set_clip(kid_ox[dp], kid_oy[dp] + POP_YOFF, - (int)kid_ow[dp], (int)kid_oh[dp]); - return; - } - x0 = kid_lx[dp] < kid_ox[dp] ? kid_lx[dp] : kid_ox[dp]; - y0 = kid_ly[dp] < kid_oy[dp] ? kid_ly[dp] : kid_oy[dp]; - x1 = kid_lx[dp] + (int)kid_lw[dp]; - y1 = kid_ly[dp] + (int)kid_lh[dp]; - if (kid_ox[dp] + (int)kid_ow[dp] > x1) x1 = kid_ox[dp] + (int)kid_ow[dp]; - if (kid_oy[dp] + (int)kid_oh[dp] > y1) y1 = kid_oy[dp] + (int)kid_oh[dp]; - pop_fore_set_clip(x0, y0 + POP_YOFF, x1 - x0, y1 - y0); -} - -/* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg): - * obj_x — ЛОГИЧЕСКАЯ X кадра (ДО ×8/7), obj_y — низ, w/h — размер. */ -static int kid_fp_x, kid_fp_y; -static uint16_t kid_fp_w, kid_fp_h; -/* Рендер-смещение по ЛОГИЧЕСКОЙ X (straddle у шва, порт xpos_in_drawn_room): - * ∓140 когда комната Kid ≠ отрисованной; 0 обычно. */ -static int kid_render_dx; -void pop_kid_set_render_dx(int dx) { kid_render_dx = dx; } -int kid_fp_obj_x(void) { return kid_fp_x; } -int kid_fp_obj_y(void) { return kid_fp_y; } -uint16_t kid_fp_width(void) { return kid_fp_w; } -uint16_t kid_fp_height(void) { return kid_fp_h; } - -/* draw_hurt_splash (seg006:2003): «брызги» урона поверх персонажа — спрайт - * chtab_2_kid id 218 (res619.png; в наших атласах это image 218, стр. 27). - * Позиция от кадра Kid: obj_y −= 15, obj_x += 5 ВПЕРЁД (obj_dx_forward). - * Кадры смерти/падения (185, 106..110) и пик (177) в оригинале смещают - * иначе — тут только общий случай + «на пиках» (порт по мере надобности). - * Рисовать ПОСЛЕ kid_draw (splash кладётся в objtable позже Kid). */ -#define KID_SPLASH_IMAGE 218 - -void kid_draw_splash(void) -{ - int obj_x = kid_fp_x, obj_y = kid_fp_y - 15; - uint8_t page = KID_SPLASH_IMAGE >> 3, idx = KID_SPLASH_IMAGE & 7; - const uint8_t *img; - uint16_t w, h; - if (page >= kid_npages) return; - if (Kid.frame == 177) /* напоролся на пики */ - obj_y = kid_fp_y, obj_x -= (Kid.direction < 0) ? -5 : 5; - else - obj_x += (Kid.direction < 0) ? -5 : 5; - obj_x = obj_x * 8 / 7; /* лог. X -> экран (calc_screen_x_coord) */ - img = (const uint8_t *)atlas_image(&kidp[page], idx); - gfx_w0_map(kidp[page].page); - w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); - h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); - if (w && h) { - uint8_t flip = (Kid.direction >= 0); - int bx = obj_x; - int top = obj_y - (int)h + 1; - if (flip) bx -= (int)w; - gfx_set_bank(GFX_BANK_SPRITE); - if (pop_onscreen_cols(bx, top + POP_YOFF, w, h)) - gfx_blit_cols_part_noclip(bx, top + POP_YOFF, img, flip, 0, 0); - else - gfx_blit_cols(bx, top + POP_YOFF, img, flip); - gfx_set_bank(GFX_BANK_NORMAL); - pop_clip_sprite(bx, w, top + POP_YOFF, (int)h); - /* Брызги — накладной спрайт: свой прямоугольник heal (см. - * kid_overlay_add), окно fore пересчитываем объединением. */ - { - uint8_t dp = gfx_get_draw_page(); - kid_overlay_add(dp, bx, top, w, h); - kid_fore_clip(dp); - } - } - gfx_w0_unmap(); -} - -/* Восстановить фон из теневой копии на месте ПРЕДЫДУЩЕГО кадра Kid. - * - * pop_heal_fast, а не голый gfx_heal: прямоугольник тут — это ровно тот - * прямоугольник, который kid_draw нарисовал noclip-ядром, то есть в - * подавляющем большинстве кадров он заведомо на экране. Клипающий путь - * остаётся для straddle (kid_render_dx = ∓140) и падения ниже поля — - * там координата реально уходит за экран. */ -void kid_heal(void) -{ - uint8_t p = gfx_get_draw_page(); - if (kid_lvalid[p]) /* +YOFF: центрирование */ - pop_heal_fast(kid_lx[p], kid_ly[p] + POP_YOFF, kid_lw[p], kid_lh[p]); - if (kid_ovalid[p]) - pop_heal_fast(kid_ox[p], kid_oy[p] + POP_YOFF, kid_ow[p], kid_oh[p]); - kid_lvalid[p] = kid_ovalid[p] = 0; -} - -void kid_draw(void) -{ - kframe fr = kid_frame; /* кадр Kid, сохранённый savekid */ - int fwd, obj_x, obj_y, top; - uint8_t page, idx; - const uint8_t *img; - uint16_t w, h; - - /* Окно fore-слоя по умолчанию пустое: на любом раннем выходе (нет - * кадра / страница не загружена) перекрывать нечего, и fore-проход - * не должен трогать экран. */ - pop_fore_set_clip(0, 0, 0, 0); - if (fr.image == 255) - return; /* пустой кадр */ - /* +kid_render_dx: straddle-смещение (∓140) когда комната Kid ≠ - * отрисованной — спрайт за кромкой. Коллизия (Kid.x в pop_map) НЕ - * смещается: она в кадре комнаты Kid; смещение только рендерное. */ - fwd = (int)Kid.x + ((Kid.direction < 0) ? -fr.dx : fr.dx) + kid_render_dx; - obj_x = (fwd << 1) - 116; - obj_y = (int)Kid.y + fr.dy; - if ((int8_t)(fr.flags ^ Kid.direction) >= 0) - obj_x++; /* even/odd pixel (в ЛОГИЧЕСКОЙ координате) */ - kid_fp_x = obj_x; /* ЛОГИЧЕСКАЯ X (до ×8/7) — для футпринта fore */ - kid_fp_y = obj_y; - /* seg008 calc_screen_x_coord: логическая X (280 шир = 28px/тайл) -> - * экран (320 шир = 32px/тайл), ×320/280 = ×8/7. БЕЗ этого Kid уезжает - * влево от тайлов тем сильнее, чем правее (col9 ~36px). Коллизия — в - * логической системе (не масштабируется). */ - obj_x = obj_x * 8 / 7; - - page = (uint8_t)(fr.image >> 3); - idx = (uint8_t)(fr.image & 7); - if (page >= kid_npages) - return; /* страница не загружена */ - img = (const uint8_t *)atlas_image(&kidp[page], idx); - gfx_w0_map(kidp[page].page); - w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); - h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); - if (w && h) { - uint8_t flip = (Kid.direction >= 0); /* dir_0_right -> зеркало */ - int bx = obj_x + KID_XOFF; - int skip = 0; /* строк спрайта срезано сверху */ - int ct, cr; - uint16_t vis_w = w; /* видимая ширина (клип справа) */ - if (flip) - bx -= (int)w; /* seg008: xpos -= image->w */ - top = obj_y - (int)h + 1 + KID_YOFF; - uint8_t dp = gfx_get_draw_page(); /* в какую страницу рисуем */ - /* clip_char (seg006:1749): тайл над головой = стена/пол → всё выше - * линии полосы своего ряда НЕ рисуется. Иначе Kid просвечивает - * сквозь щели кладки при спуске/висе. - * - * Окно Char обязательно: clip_char работает с АКТИВНЫМ персонажем - * (в оригинале он и зовётся внутри add_kid_to_objtable, то есть под - * loadkid), а к моменту отрисовки в Char лежит тот, кто тикал - * последним — страж. Выгружать назад нечего: clip_char только - * читает. */ - pop_loadkid(); - ct = pop_clip_char_top(kid_fp_x, obj_y, w, h); - if (ct > top) { - skip = ct - top; - if (skip >= (int)h) skip = (int)h; /* обрезан целиком */ - } - /* clip_char, вторая половина: правый косяк двери уровня. Дверь - * рисуется ПОД персонажем (вся в backtable), поэтому спрятать его - * можно только срезав спрайт — см. pop_clip_char_right. */ - cr = pop_clip_char_right(); - if (cr) { - int avail = cr - bx; - if (avail <= 0) vis_w = 0; /* весь спрайт за косяком */ - else if (avail < (int)w) vis_w = (uint16_t)avail; - } - gfx_set_bank(GFX_BANK_SPRITE); /* видео-ОЗУ, без теневой копии */ - if (skip < (int)h && vis_w) { /* +YOFF: центрирование */ - /* noclip-ядро ширину не ограничивает, поэтому при обрезке справа - * идём общим путём: это редкие кадры (подъём в двери уровня раз - * за уровень), цена подготовки тут не видна. */ - if (vis_w == w && pop_onscreen_cols(bx, top + skip + POP_YOFF, w, - (uint16_t)((int)h - skip))) - gfx_blit_cols_part_noclip(bx, top + skip + POP_YOFF, img, flip, - (uint8_t)skip, 0); - else - gfx_blit_cols_part_w(bx, top + skip + POP_YOFF, img, flip, - skip, 0, (uint8_t)(vis_w == w ? 0 : vis_w)); - } - gfx_set_bank(GFX_BANK_NORMAL); - /* клип СВОЕГО выхода за поле (падение ниже поля / верхний борт) — - * узко, только по прямоугольнику Кида и только при выходе. */ - pop_clip_sprite(bx, w, top + skip + POP_YOFF, (int)h - skip); - /* heal чистит ТОЛЬКО нарисованное: иначе стирается кромка пола над - * срезом (мусор/дыра в кладке на второй странице дабл-буфера). */ - kid_lx[dp] = bx; kid_ly[dp] = top + skip; - kid_lw[dp] = vis_w; kid_lh[dp] = (uint16_t)((int)h - skip); - kid_lvalid[dp] = (skip < (int)h && vis_w != 0); - kid_fp_w = w; kid_fp_h = h; /* размер кадра для футпринта fore */ - } - /* add_sword_to_objtable (seg006:1798): клинок отдельным спрайтом поверх - * Kid — со СВОИМ прямоугольником heal (kid_overlay_add). */ - { - int sx, sy; uint16_t sw, sh; - if (pop_sword_draw(&Kid, &fr, obj_x, obj_y, &sx, &sy, &sw, &sh)) - kid_overlay_add(gfx_get_draw_page() & 1, sx, sy, sw, sh); - } - gfx_w0_unmap(); - /* Окно fore-слоя = объединение «Kid + клинок» (с обрезкой clip_char): - * fore-проход вернёт куски тайлов ТОЛЬКО поверх него, а не целыми - * тайлами — см. pop_bg.c, fclip_on. Ставим и когда рисовать нечего: - * нулевое окно = fore-проход не трогает экран, что и правильно. */ - kid_fore_clip(gfx_get_draw_page() & 1); -} - -/* Вернуть окно fore-клипа на прямоугольник ПОСЛЕДНЕГО кадра Kid. - * - * Окно (pop_fore_set_clip) — одно на всех, и его ставит КАЖДЫЙ, кто рисует - * персонажа: kid_draw в конце — своё, pop_guard_draw в конце — своё. Пока - * страж рисовался строго ДО Кида, к моменту pop_fore_over_kid в окне - * оказывался Kid, и всё сходилось само. Как только порядок стал - * переменным (guard_over_kid в roomtest.c — порядок задаёт обход тайлов), - * при «страж поверх» окно оставалось СТРАЖЬИМ и fore-проход Кида отсекался - * целиком: Кид рисовался поверх передних столбов. - * - * Поэтому окно восстанавливается явно перед pop_fore_over_kid, а не - * держится «по счастливому порядку вызовов». */ -void kid_fore_clip_restore(void) -{ - kid_fore_clip(gfx_get_draw_page() & 1); -} +/* Отрисовка кадра (load_frame_to_obj, heal, клип, брызги, клинок) живёт в + * pop_cdraw.c — ОДНА на всех Char, см. DRAW-CHAR. Отсюда она получает + * атласы Кида (kidp/kid_npages) и его кадр (kid_frame). */ diff --git a/applications/PoP/roomtest/pop_kid.h b/applications/PoP/roomtest/pop_kid.h index 3a8e44a..ede4d74 100644 --- a/applications/PoP/roomtest/pop_kid.h +++ b/applications/PoP/roomtest/pop_kid.h @@ -13,6 +13,7 @@ #include #include "pop_char.h" #include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */ +#include /* atlas_t — набор спрайтов Кида (см. kidp ниже) */ /* Состояние персонажа — общий тип для Кида, стража и Char (pop_char.h). */ extern pop_char_t Kid; @@ -114,41 +115,16 @@ uint8_t kid_cur_flags(void); * точка для pop_ctrl/pop_map (иначе каждый дублировал бы kid_seqtbl ~2.3КБ). */ void kid_set_seq(uint8_t seq_id); -/* Рендер-смещение Kid по ЛОГИЧЕСКОЙ X (порт xpos_in_drawn_room, seg004): - * когда комната Kid ≠ отрисованной (straddle у шва), спрайт рисуется со - * сдвигом ∓140 (левый сосед −140 / правый +140), при этом Kid.x/коллизия - * остаются в кадре комнаты Kid. 0 — комнаты совпадают (обычный случай). - * Влияет на kid_draw и метрики футпринта (fore) — heal берёт запомненный - * прямоугольник, поэтому согласован автоматически. */ -void pop_kid_set_render_dx(int dx); - -/* Нарисовать текущий кадр Kid (банк GFX_BANK_SPRITE — видео-ОЗУ, без - * теневой копии; запоминает прямоугольник для kid_heal). */ -void kid_draw(void); - -/* Вернуть окно fore-клипа на прямоугольник последнего кадра Kid. Звать - * ПЕРЕД pop_fore_over_kid, если между ним и kid_draw кто-то ещё рисовал - * персонажа (pop_guard_draw ставит окно по стражу). */ -void kid_fore_clip_restore(void); - -/* draw_hurt_splash (seg006:2003): «брызги» урона (image 218) поверх Kid — - * звать СРАЗУ после kid_draw в кадре, когда HP убавилось (pop_kid_hurt). */ -void kid_draw_splash(void); - /* Блит спрайта chtab_2 по id, левым верхним углом в (x,y) — для полосы HP * (id 216 полное деление / 217 пустое). */ void pop_kid_img_blit(uint8_t image, int x, int y); -/* Восстановить фон (из теневой копии) на месте ПРЕДЫДУЩЕГО кадра Kid — - * вместо перерисовки всего экрана. Звать ПЕРЕД kid_tick/kid_draw. */ -void kid_heal(void); - -/* Метрики последнего kid_draw для футпринта fore-over-Kid (pop_bg - * pop_fore_over_kid): obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ - * спрайта, w/h — размер кадра. */ -int kid_fp_obj_x(void); -int kid_fp_obj_y(void); -uint16_t kid_fp_width(void); -uint16_t kid_fp_height(void); +/* Атласы Кида (chtab_2) и его текущий кадр — набор слота POP_CH_KID общей + * отрисовки персонажей (pop_cdraw.c). Наружу отданы как данные, а не через + * аксессоры: читаются раз в кадр из другого банка, где вызов стоил бы + * дороже самого чтения (данные в _DATA видны всем без трамплина). */ +extern atlas_t kidp[]; +extern uint8_t kid_npages; +extern kframe kid_frame; #endif diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 2c23264..9dace8b 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -33,6 +33,14 @@ #include "pop_level.h" /* pop_level_start_room — вход в дверь уровня */ #include "pop_guard.h" /* SWORD_2_DRAWN — ветка приземления с мечом */ #include "pop_tune.h" /* POP_ENABLE_JUMP_GRAB и прочие настройки */ +#include "pop_cdraw.h" /* габарит кадра персонажа (pop_cd) */ + +/* Габарит последнего нарисованного кадра АКТИВНОГО персонажа (char_width_half + * и char_top_y в set_char_collision, seg006:0723). У каждого слота он свой: + * до DRAW-CHAR отрисовка была продублирована, метрики Кида лежали отдельно, и + * коллизия СТРАЖА считалась по ширине спрайта КИДА. Отставание на кадр — + * как в оригинале (cur_frame берётся тем же кадром, что рисуется). */ +#define CD_ACT (&pop_cd[Char.charid == CHARID_0_KID ? POP_CH_KID : POP_CH_OPP]) /* ---- Тайлы / действия / seq id (подмножество PoP) ------------------ */ #define TILE_EMPTY 0 @@ -307,7 +315,7 @@ static uint8_t gate_passable(int8_t col, int8_t row) { uint8_t gm = gate_modif(col, row); if (gm == 0xFF) return 0; - return (uint8_t)((gm >> 2) + 6) >= (uint8_t)kid_fp_height(); + return (uint8_t)((gm >> 2) + 6) >= (uint8_t)CD_ACT->fph; } static uint8_t tile_is_floor(uint8_t t) @@ -1368,7 +1376,7 @@ static void pop_coll_invalidate(void) { coll_prev_row = COLL_ROW_NONE; } static void set_char_collision(void) { int e = char_x_forward_edge(); - int wh = ((int)kid_fp_width() + 1) >> 1; /* char_width_half */ + int wh = ((int)CD_ACT->fpw + 1) >> 1; /* char_width_half */ if (Char.direction < 0) { coll_xl = e; coll_xr = e + wh; } else { coll_xr = e; coll_xl = e - wh; } if (kid_cur_flags() & FRAME_THIN) { coll_xl += 4; coll_xr -= 4; } @@ -2073,13 +2081,14 @@ static void check_spiked(void) * спрайта (char_x_left..char_x_right, порт set_char_collision) скан ВНИЗ от * curr_row через НЕ-floor тайлы; встретил пику → start_anim_spike. Габарит, * а не «curr_col + колонка спереди»: иначе пики выдвигаются на тайл раньше - * (когда тело Кида ещё не над шахтой). Метрики кадра — kid_fp (габарит - * последнего kid_draw; отставание в 1 кадр несущественно, ошибка в позднюю - * сторону). */ + * (когда тело Кида ещё не над шахтой). Метрики кадра — pop_cd (габарит + * последнего нарисованного кадра; отставание в 1 кадр несущественно, ошибка + * в позднюю сторону). */ static void check_spike_below(void) { - int wh = ((int)kid_fp_width() + 1) >> 1; /* char_width_half */ - int xl = kid_fp_obj_x() / 2 + 58; /* char_x_left */ + const pop_cdraw_t *cd = CD_ACT; + int wh = ((int)cd->fpw + 1) >> 1; /* char_width_half */ + int xl = cd->fpx / 2 + 58; /* char_x_left */ int xr; int8_t c0, c1, col; if (Char.direction >= 0) xl -= wh; /* лицом вправо: левый край внутрь */ diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index b280ba7..5fa216e 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -118,7 +118,8 @@ extern uint8_t pop_ceil_fell; extern uint8_t pop_debris_at; /* Взводится в кадре, когда Kid получил урон (hitp_delta<0 оригинала) — - * приложение рисует «брызги» kid_draw_splash() поверх Kid и снимает флаг. */ + * отрисовка персонажа рисует «брызги» поверх Kid, а главный цикл снимает + * флаг. */ extern uint8_t pop_kid_hurt; /* check_knock (seg003): по флагу knock (play_seq) тряхнуть loose-полы ряда @@ -158,7 +159,7 @@ void pop_guard_phys_tick(void) __banked; * над головой — стена/пол, всё выше линии своей полосы не рисуется (Kid, * ушедший головой под пол при спуске/висе, не просвечивает сквозь щели). * obj_x — ЛОГИЧЕСКАЯ X кадра (до ×8/7), obj_y — низ спрайта, w/h — размер. - * Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт kid_draw. */ + * Возврат: КОМНАТНЫЙ y границы, 0 = клипа нет. Зовёт pop_char_draw. */ int pop_clip_char_top(int obj_x, int obj_y, uint16_t w, uint16_t h) __banked; /* Парная половина clip_char: ЭКРАННЫЙ X, правее которого спрайт не рисуется diff --git a/applications/PoP/roomtest/pop_redraw.h b/applications/PoP/roomtest/pop_redraw.h index b94846b..690d60b 100644 --- a/applications/PoP/roomtest/pop_redraw.h +++ b/applications/PoP/roomtest/pop_redraw.h @@ -42,7 +42,7 @@ void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages); void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages); /* Разобрать пометки: нарисовать всё помеченное на ТЕКУЩЕЙ (back) странице и - * уменьшить счётчики. Звать из главного цикла в слое ФОНА — до kid_draw. */ + * уменьшить счётчики. Звать из главного цикла в слое ФОНА — до персонажей. */ void pop_redraw_needed(void); /* Сбросить все пометки (смена комнаты/уровня). */ diff --git a/applications/PoP/roomtest/pop_tile.c b/applications/PoP/roomtest/pop_tile.c new file mode 100644 index 0000000..ef3394c --- /dev/null +++ b/applications/PoP/roomtest/pop_tile.c @@ -0,0 +1,240 @@ +/* + * pop_tile.c — общие ЛИСТЬЯ слоя фона: блит куска атласа, чтение тайла + * комнаты, таблица тайлов. РЕЗИДЕНТ W1 (_CODE). + * + * Мотивация раскладки (почему резидент, а не банк) — в шапке pop_tile.h: + * коротко, эти функции и таблицы нужны ОБЕИМ половинам слоя фона (горячей — + * fore-проход каждый кадр, холодной — полная отрисовка комнаты), а код и + * rodata банка видны только пока банк замаплен. W1 замаплено всегда. + */ +#include +#include +#include +#include "pop_bg.h" /* POP_YOFF */ +#include "pop_tile.h" +#include "pop_level.h" /* pop_doorlink2 — подстановка нажатой кнопки */ +#include "_pop_draw.h" /* pop_heal_fast */ + +/* ---- Состояние (писучее, _DATA/W2 — видно из любого банка) ---------- */ +atlas_t pop_env[5], pop_wall_at, pop_fore_at, pop_pot_at; + +const uint8_t *pop_t_fg, *pop_t_bg, *pop_t_lfg, *pop_t_lbg; +const uint8_t *pop_t_below, *pop_t_above, *pop_t_abovemod; +uint8_t pop_t_room; +pop_rnd_t pop_t_seed; + +int pop_t_clip_top; +int pop_t_fclip_x0, pop_t_fclip_y0, pop_t_fclip_x1, pop_t_fclip_y1; +uint8_t pop_t_fclip_on; +uint8_t pop_t_bake_rest; + +/* Позиция склянки внутри тайла зелья — поля fore_x/fore_y записи 0x0A той же + * таблицы. Вынесены МАКРОСАМИ (одно место правки, дублирования данных нет): + * пока таблица была `static const`, SDCC сворачивала `tile_table[10].fore_x` + * в константу, но глобальную (её читают оба банка) уже не сворачивает — + * чтение полей в рантайме стоило +150 Б и IX-кадр на 13 локалов. */ +#define POT_FORE_X 2 +#define POT_FORE_Y (-3) + +/* ---- Таблица тайлов (piece, seg008.c:27) --------------------------- */ +const piece pop_tile_table[31] = { + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, /* 00 empty */ + { 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 01 floor */ + {127, 1, 0, 133, 1, 2, 145, 0, 43, 0, 0, 0}, /* 02 spike */ + { 92, 1, 0, 93, 1, 2, 0, 94, 43, 95, 1, 0}, /* 03 pillar */ + { 46, 1, 0, 47, 1, 2, 0, 48, 43, 49, 3, 0}, /* 04 gate */ + { 41, 1, 1, 35, 1, 3, 145, 0, 36, 0, 0, 0}, /* 05 stuck floor */ + { 41, 1, 0, 42, 1, 2, 145, 0, 96, 0, 0, 0}, /* 06 close button */ + { 46, 1, 0, 0, 0, 2, 0, 0, 43, 49, 3, 0}, /* 07 door top w/floor */ + { 86, 1, 0, 87, 1, 2, 0, 0, 43, 88, 1, 0}, /* 08 big pillar bottom */ + { 0, 0, 0, 89, 0, 3, 0, 90, 0, 91, 1, 3}, /* 09 big pillar top */ + { 41, 1, 0, 42, 1, 2, 145, 0, 43, 12, POT_FORE_X, POT_FORE_Y}, /* 0A potion */ + { 0, 1, 0, 0, 0, 0, 145, 0, 0, 0, 0, 0}, /* 0B loose floor */ + { 0, 0, 0, 0, 0, 2, 0, 0, 85, 49, 3, 0}, /* 0C door top */ + { 75, 1, 0, 42, 1, 2, 0, 0, 43, 77, 0, 0}, /* 0D mirror */ + { 97, 1, 0, 98, 1, 2, 145, 0, 43,100, 0, 0}, /* 0E debris */ + {147, 1, 0, 42, 1, 1, 145, 0,149, 0, 0, 0}, /* 0F open button */ + { 41, 1, 0, 37, 0, 0, 0, 38, 43, 0, 0, 0}, /* 10 leveldoor left */ + { 0, 0, 0, 39, 1, 2, 0, 40, 43, 0, 0, 0}, /* 11 leveldoor right */ + { 0, 0, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 12 chomper */ + { 41, 1, 0, 42, 1, 2, 0, 0, 43, 0, 0, 0}, /* 13 torch */ + { 0, 0, 0, 1, 1, 2, 0, 2, 0, 0, 0, 0}, /* 14 wall */ + { 30, 1, 0, 31, 1, 2, 0, 0, 43, 0, 0, 0}, /* 15 skeleton */ + { 41, 1, 0, 42, 1, 2, 145, 0, 43, 0, 0, 0}, /* 16 sword */ + { 41, 1, 0, 10, 0, 0, 0, 11, 43, 0, 0, 0}, /* 17 balcony left */ + { 0, 0, 0, 12, 1, 2, 0, 13, 43, 0, 0, 0}, /* 18 balcony right */ + { 92, 1, 0, 42, 1, 2, 145, 0, 43, 95, 1, 0}, /* 19 lattice pillar */ + { 1, 0, 0, 0, 0, 0, 0, 0, 2, 9, 0,-53}, /* 1A lattice down */ + { 3, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1B lattice small */ + { 4, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1C lattice left */ + { 5, 0,-10, 0, 0, 0, 0, 0, 0, 9, 0,-53}, /* 1D lattice right */ + { 97, 1, 0, 98, 1, 2, 0, 0, 43,100, 0, 0}, /* 1E debris w/torch */ +}; + +const uint8_t POP_COL_XH[10] = {0, 4, 8, 12, 16, 20, 24, 28, 32, 36}; +const uint8_t POP_WALL_FRAM_BOTTOM[4] = {7, 9, 5, 3}; +const uint8_t POP_WALL_FRAM_MAIN[4] = {8, 10, 6, 4}; +const uint8_t POP_SPIKES_FRAM_LEFT[10] = {0, 128, 129, 130, 131, 132, 131, 129, 128, 0}; +const uint8_t POP_LOOSE_FRAM_LEFT[12] = {41,69,41,70,70,41,41,41,70,70,70,0}; +const uint8_t POP_LOOSE_FRAM_BOTTOM[12] = {43,73,43,74,74,43,43,43,74,74,74,0}; + +/* get_loose_frame (seg008:761): индекс дрожащего кадра по модификатору. + * В режиме ЗАПЕЧКИ фона отдаёт кадр ПОКОЯ — транзиентный кадр нельзя + * консервировать в ОЗУ-копию (см. pop_t_bake_rest в pop_bg.c). */ +uint8_t pop_loose_frame(uint8_t m) +{ + if (pop_t_bake_rest) return 0; + if (m & 0x80) { m &= 0x7F; if (m > 10) return 1; } + return m; +} + +/* ---- Блит куска атласа ---------------------------------------------- */ +void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom) +{ + const uint8_t *img; + uint16_t w, h; + int top, dx, dy, dw, dh, sx = 0, sy = 0; + if (idx >= a->count) + return; + /* Грубый отсев ДО маппинга страницы: реальные w/h лежат в заголовке + * ленты, то есть в EMM-странице, и чтобы их прочитать, надо сделать + * gfx_w0_map. Берём заведомо больший габарит (кадры фона не крупнее + * 64×64) и, если он не задевает окно, уходим не заплатив ни за + * atlas_image, ни за map/unmap. */ + if (pop_t_fclip_on && + (x >= pop_t_fclip_x1 || x + 64 <= pop_t_fclip_x0 || + ybottom + POP_YOFF + 1 <= pop_t_fclip_y0 || + ybottom + POP_YOFF - 64 >= pop_t_fclip_y1)) + return; + img = (const uint8_t *)atlas_image(a, idx); + gfx_w0_map(a->page); + w = (uint16_t)(img[0] | ((uint16_t)img[1] << 8)); + h = (uint16_t)(img[2] | ((uint16_t)img[3] << 8)); + top = ybottom - (int)h + 1 + POP_YOFF; /* +YOFF: центрирование */ + dx = x; dy = top; dw = (int)w; dh = (int)h; + if (pop_t_clip_top && dy < pop_t_clip_top) { /* полоса у потолка */ + sy = pop_t_clip_top - dy; dy = pop_t_clip_top; dh -= sy; + } + if (pop_t_fclip_on) { /* окно fore-слоя */ + if (dx < pop_t_fclip_x0) { int d = pop_t_fclip_x0 - dx; sx += d; dx += d; dw -= d; } + if (dy < pop_t_fclip_y0) { int d = pop_t_fclip_y0 - dy; sy += d; dy += d; dh -= d; } + if (dx + dw > pop_t_fclip_x1) dw = pop_t_fclip_x1 - dx; + if (dy + dh > pop_t_fclip_y1) dh = pop_t_fclip_y1 - dy; + } + if (w && h && dw > 0 && dh > 0) { + /* Целиком на экране — линейное ядро без клипа: ~2.9× быстрее + * (замер: 4617 против 13288 тактов на спрайт 32×3; общее ядро + * gfx_blit платит за клип/16-бит/split, а не за пиксели). + * W3-скобку ставит сама libbgi: из кода, собранного с --w3, её + * вызывать нельзя (окно W3 уходит под видеобанк). */ + if (dx >= 0 && dy >= 0 && dw < 256 && dh < 256 && + dx + dw <= 320 && dy + dh <= 256 && w < 256) { + if (sx == 0 && sy == 0 && dw == (int)w && dh == (int)h) + gfx_blit_noclip(dx, dy, img); + else + gfx_blit_part_noclip(dx, dy, img, (uint8_t)sx, (uint8_t)sy, + (uint8_t)dw, (uint8_t)dh); + } else { + gfx_blit_part(dx, dy, img, sx, sy, dw, dh); /* край экрана — с клипом */ + } + } + gfx_w0_unmap(); +} + +void pop_env_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_env[id >> 5], (uint8_t)(id & 31), x, yb); } +void pop_wall_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_wall_at, id, x, yb); } +void pop_fore_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_fore_at, id, x, yb); } +void pop_pot_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_pot_at, id, x, yb); } + +void pop_heal_off(int x, int y, int w, int h) +{ + pop_heal_fast(x, y + POP_YOFF, w, h); +} + +/* ---- Чтение тайла отрисованной комнаты ------------------------------ */ +uint8_t pop_tile_code(int row, int col) +{ + if (col < 0) { + if (pop_t_lfg && row >= 0 && row < 3) return pop_t_lfg[row]; + return 20; /* уровневая кромка = стена */ + } + if (row < 0) { /* верхняя полоса: ряд 2 комнаты сверху */ + if (row == -1 && pop_t_above && col < 10) return pop_t_above[col]; + return 1; /* нет комнаты сверху → пол (кромка) */ + } + if (row > 2 || col > 9) return 20; + return pop_t_fg[row * 10 + col]; +} + +uint8_t pop_tile_mod(int row, int col) +{ + if (col < 0) { + if (pop_t_lbg && row >= 0 && row < 3) return pop_t_lbg[row]; + return 0; + } + if (row == -1 && pop_t_abovemod && col < 10) return pop_t_abovemod[col]; + if (row < 0 || row > 2 || col > 9) return 0; + return pop_t_bg[row * 10 + col]; +} + +/* Порт подстановки нажатой кнопки из get_tile_to_draw (seg008:240): opener → + * floor, closer → stuck, когда таймер связи > 1. Любой слой, который + * выбирает ветку отрисовки по типу тайла, обязан смотреть на ПОДСТАВЛЕННЫЙ + * тип (иначе кнопка, на которой стоит Kid, ведёт себя как «не пол» — напр. + * climb-оверлей уходил в draw_other_overlay и закрашивал Kid целым тайлом + * вместо узкой кромки floor_left_overlay). */ +uint8_t pop_tile_code_drawn(int row, int col) +{ + uint8_t code = pop_tile_code(row, col); + 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; } + return code; +} + +uint8_t pop_wall_modifier(int row, int col) +{ + uint8_t lw = (col > 0) ? (pop_tile_code(row, col - 1) == 20) : 1; + uint8_t rw = (col < 9) ? (pop_tile_code(row, col + 1) == 20) : 1; + if (lw && rw) return 3; /* WWW */ + if (lw) return 2; /* WWS */ + if (rw) return 1; /* SWW */ + return 0; /* SWS */ +} + +/* ---- Зелье: склянка (tiles_10_potion, seg008 draw_tile_fore) --------- * + * Тип = (modif & 0xF8) >> 3: 0 = пусто (ничего не рисуем), 2..4 — «большая» + * склянка (id 13), остальные — маленькая (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. + * + * Здесь, а не в банке: склянку рисует и полная отрисовка тайла (холодная + * половина), и fore-проход поверх персонажа (горячая). */ +void pop_potion_flask(int row, int col, uint8_t modif) +{ + uint8_t type = (uint8_t)((modif & 0xF8) >> 3); + uint8_t id = 12; + if (type == 0) return; + if (type >= 2 && type < 5) id = 13; /* большая склянка */ + pop_pot_b(id, (POT_FORE_X + POP_COL_XH[col]) * 8 + 6, + POT_FORE_Y + 63 * row + 62); +} + +/* Окно fore-клипа: ставит тот, кто нарисовал персонажа (pop_cdraw, банк 4), + * читает blit_b. Резидент — чтобы вызов из банка шёл без трамплина. */ +void pop_fore_set_clip(int x, int y, int w, int h) +{ + pop_t_fclip_x0 = x; pop_t_fclip_y0 = y; + pop_t_fclip_x1 = x + w; pop_t_fclip_y1 = y + h; +} + +/* Ряд 0 комнаты СНИЗУ (fg-коды) — нужен draw_tile_topright/floorright, когда + * тайл нижнего ряда стал видимым (empty после падения loose): «стена вниз» = + * топрайт тайла room_B(0, col-1). 0 — нет комнаты снизу (кромка = стена). */ +void pop_room_set_below(const uint8_t *below_row0_fg) { pop_t_below = below_row0_fg; } + +/* Ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки у + * потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom + * при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */ +void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) +{ pop_t_above = above_row2_fg; pop_t_abovemod = above_row2_mod; } diff --git a/applications/PoP/roomtest/pop_tile.h b/applications/PoP/roomtest/pop_tile.h new file mode 100644 index 0000000..b4dc255 --- /dev/null +++ b/applications/PoP/roomtest/pop_tile.h @@ -0,0 +1,121 @@ +/* + * pop_tile.h — ОБЩИЕ ЛИСТЬЯ отрисовки фона: блит куска атласа, чтение тайла + * комнаты, таблица тайлов. Внутренний заголовок слоя фона, НЕ публичный + * API (публичный — pop_bg.h). + * + * ПОЧЕМУ ОТДЕЛЬНЫЙ РЕЗИДЕНТНЫЙ МОДУЛЬ (pop_tile.c в _CODE / W1), а не часть + * банка 2. Слой фона режется надвое: горячая половина (fore-проход поверх + * персонажей, каждый кадр) и холодная (полная отрисовка комнаты и точечные + * перерисовки тайлов, раз на комнату/событие). Обе половины зовут одни и те + * же листья — blit_b, tile_code, tile_table. Разложить их по банкам нельзя: + * + * - код в чужом банке зовётся ТОЛЬКО через трамплин (по `__banked` в + * объявлении), и пометка бьёт по ВСЕМ вызывающим, включая горячих; + * - `const`-таблицы банка лежат в его СТРАНИЦЕ (0xC000+) и видны, лишь + * пока банк замаплен — из другого банка их не прочитать вовсе; + * - дублировать листья в оба банка — лишние килобайты и два места правки. + * + * Резидент W1 замаплен ВСЕГДА, поэтому и горячая, и холодная половина зовут + * их обычным `call` без трамплина, а таблицы читают напрямую. Тот же приём, + * что у pop_heal_fast (pop_draw.c) и pop_sword_draw (pop_kid.c). + * + * ПИСУЧИЕ глобалы (атласы, указатели на комнату, окно клипа) лежат в _DATA + * (W2) и видны отовсюду независимо от банка — они здесь ради одного места + * определения, а не ради доступности. + */ +#ifndef POP_TILE_H +#define POP_TILE_H + +#include +#include +#include "pop_geom.h" /* pop_rnd_t — сид кладки комнаты */ + +/* ---- Атласы фона (прямая адресация id) ------------------------------ * + * env[id>>5] idx id&31 — chtab_6 environment; wall — chtab_7; fore — + * передний слой; pot — chtab_1 (зелья, меч-предмет). */ +extern atlas_t pop_env[5], pop_wall_at, pop_fore_at, pop_pot_at; + +/* ---- Текущая отрисованная комната ----------------------------------- * + * fg — foretable-коды, bg — модификаторы; lfg/lbg — правый столбец ЛЕВОГО + * соседа (кромка); below — ряд 0 комнаты снизу (11 байт, см. pop_bg.h); + * above/abovemod — ряд 2 комнаты сверху (полоса кладки у потолка). */ +extern const uint8_t *pop_t_fg, *pop_t_bg, *pop_t_lfg, *pop_t_lbg; +extern const uint8_t *pop_t_below, *pop_t_above, *pop_t_abovemod; +extern uint8_t pop_t_room; /* номер комнаты — зерно раскладки стен */ +extern pop_rnd_t pop_t_seed; /* сид кладки (seg009 PRNG) */ + +/* Верхний клип блитов фона (ЭКРАННАЯ y): !=0 — строки кадра выше границы не + * рисуются. Нужен полосе кладки у потолка (ряд −1): высокие куски не должны + * лезть в верхний борт. 0 — без клипа. */ +extern int pop_t_clip_top; + +/* ОКНО FORE-СЛОЯ (экранные координаты, полуинтервал). Пока включено, + * blit_b рисует только ту часть куска, что попадает в окно. + * + * Зачем: fore-слой существует РОВНО для того, чтобы вернуть куски тайлов + * поверх нарисованного персонажа — за пределами его прямоугольника в + * видеопамяти и так лежит правильный фон. А рисовал он тайлы ЦЕЛИКОМ: + * стена (код 20) — блок 64×63 = 4032 пикселя, и таких кусков за проход до + * девяти. Замер в MAME 2026-07-29 (комната 3, Kid + страж): проход поверх + * Kid — 402 816 тактов, поверх стража — 431 964, вместе 78 % всей работы + * кадра при бюджете 430 000. С окном большинство кусков отсекается целиком, + * остальные режутся до пересечения. */ +extern int pop_t_fclip_x0, pop_t_fclip_y0, pop_t_fclip_x1, pop_t_fclip_y1; +extern uint8_t pop_t_fclip_on; + +/* ---- Таблица тайлов (piece, seg008.c:27) ---------------------------- */ +typedef struct { + uint8_t base_id, floor_left; int8_t base_y; + uint8_t right_id, floor_right; int8_t right_y; + uint8_t stripe_id, topright_id, bottom_id, fore_id, fore_x; int8_t fore_y; +} piece; + +extern const piece pop_tile_table[31]; + +/* Таблицы, нужные ОБЕИМ половинам слоя фона (односторонние живут по месту). */ +extern const uint8_t POP_COL_XH[10]; /* x тайла = COL_XH[col]*8 */ +extern const uint8_t POP_WALL_FRAM_BOTTOM[4]; /* нижняя грань стены по bg */ +extern const uint8_t POP_WALL_FRAM_MAIN[4]; /* основная грань стены */ +extern const uint8_t POP_SPIKES_FRAM_LEFT[10]; /* кадр пик в своей ячейке */ +extern const uint8_t POP_LOOSE_FRAM_LEFT[12]; /* верх дрожащей плиты */ +extern const uint8_t POP_LOOSE_FRAM_BOTTOM[12]; /* низ дрожащей плиты */ + +/* Режим ЗАПЕЧКИ статического фона (запись идёт и в ОЗУ-копию акселератора, + * из которой потом восстанавливает heal). В этом режиме дрожащие кадры + * ЧУЖИХ плит рисовать НЕЛЬЗЯ: транзиентный кадр законсервируется в копии, а + * так как запечка идёт двумя кадрами (по одному на страницу дабл-буфера), + * на страницах застынут РАЗНЫЕ кадры — вечное мерцание. Фон обязан быть + * состоянием ПОКОЯ; анимация каждый кадр кладётся поверх (банк SPRITE). */ +extern uint8_t pop_t_bake_rest; + +/* Кадр дрожащей/падающей плиты по её room_modif (seg007 loose_fram_*). */ +uint8_t pop_loose_frame(uint8_t modif); + +/* ---- Блит куска атласа ---------------------------------------------- * + * НИЗ спрайта на ybottom (room-space, POP_YOFF добавляется внутри), левый + * край на x, прозрачно (0xFF). w/h читаются из getimage-заголовка ленты (в + * W0). Режется окном fore-клипа и pop_t_clip_top. */ +void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom); +void pop_env_b(uint8_t id, int x, int yb); +void pop_wall_b(uint8_t id, int x, int yb); +void pop_fore_b(uint8_t id, int x, int yb); +void pop_pot_b(uint8_t id, int x, int yb); + +/* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. */ +void pop_heal_off(int x, int y, int w, int h); + +/* ---- Чтение тайла отрисованной комнаты ------------------------------ */ +uint8_t pop_tile_code(int row, int col); +uint8_t pop_tile_mod(int row, int col); +/* Тип «как он РИСУЕТСЯ» — подстановка нажатой кнопки (get_tile_to_draw, + * seg008:240): opener → floor, closer → stuck при таймере связи > 1. */ +uint8_t pop_tile_code_drawn(int row, int col); +/* seg008.c:1255: сосед — стена, если его тип == 20. 0 SWS / 1 SWW / 2 WWS + * / 3 WWW. На кромке col0 сосед по умолчанию = стена (как render_room). */ +uint8_t pop_wall_modifier(int row, int col); + +/* Склянка зелья (draw_tile_fore, chtab_1): нужна и полной отрисовке тайла, и + * fore-проходу поверх персонажа. */ +void pop_potion_flask(int row, int col, uint8_t modif); + +#endif diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index 382ca5f..3e22afc 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -479,7 +479,7 @@ void pop_process_trobs(uint8_t cur_room) __banked /* ПИКИ — редрой КАЖДЫЙ кадр, пока trob жив: animate_spike * (seg007:0353) зовёт redraw_21h БЕЗУСЛОВНО, а не по смене кадра. * Это не «лишняя работа оригинала»: выдвинутая пика живёт только в - * видео-ОЗУ (банк SPRITE, в фон не запечена), а kid_heal каждый кадр + * видео-ОЗУ (банк SPRITE, в фон не запечена), а heal персонажа каждый кадр * возвращает под спрайтом Кида ПЕЧЁНЫЙ фон — то есть стирает те * части остриёв, что попали под его прямоугольник. При редрое * по смене кадра пика держится 15 кадров (modif 0x8F..0x81) с @@ -502,7 +502,7 @@ void pop_process_trobs(uint8_t cur_room) __banked if (room == cur_room && code == TILE_LEVELDOOR) { /* Створка едет каждый кадр — перерисовываем ПРАВУЮ половину (там * её рисует draw_tile_anim_right) С ЗАПЕЧКОЙ в ОЗУ-копию: иначе - * kid_heal вернул бы из фона закрытую створку. Куски двери + * heal персонажа вернул бы из фона закрытую створку. Куски двери * непрозрачны, поэтому wipe не нужен. */ /* Створку рисует ПРАВАЯ половина двери; последний кадр анимации * помечаем на обе страницы, иначе дверь дрожит через кадр. */ diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index 8f60500..fc2190c 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -18,6 +18,7 @@ #include "pop_redraw.h" #include "pop_geom.h" /* pop_y_to_row, TILE_SIZEX — тайл объекта */ #include "pop_guard.h" +#include "pop_cdraw.h" /* общая отрисовка персонажей (слоты Кид/соперник) */ #include "pop_cheat.h" #include "pop_tune.h" @@ -50,7 +51,7 @@ __sfr __at (0xFE) io_border; /* Сколько банков грузит crt0_banked (читает его стартовый код). * 1 = guards (ИИ/боёвка), 2 = pop_bg (фон, fore, loose), 3 = pop_map - * (коллизия/физика), 4 = pop_gdraw (отрисовка стража + полоса HP), + * (коллизия/физика), 4 = pop_cdraw (отрисовка персонажей + полоса HP), * 5 = pop_ctrl (диспетчер управления). * Отрисовка стража вынесена из банка 2 отдельно: тот подошёл к 16 КБ * вплотную (16 021 из 16 384), а расти ему ещё есть куда — тайлы поздних @@ -127,7 +128,7 @@ static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_ro /* Рендер-смещение Kid по логической X (порт xpos_in_drawn_room): комната Kid * слева от отрисованной → −140 (спрайт за левой кромкой), справа → +140, та - * же → 0. Звать каждый кадр перед kid_draw. */ + * же → 0. Звать каждый кадр перед отрисовкой персонажей. */ static void update_kid_render_dx(void) { int dx = 0; @@ -135,7 +136,7 @@ static void update_kid_render_dx(void) if (kid_room == pop_room_link(cur_room, 0)) dx = -140; /* left neighbour */ else if (kid_room == pop_room_link(cur_room, 1)) dx = 140; /* right neighbour */ } - pop_kid_set_render_dx(dx); + pop_char_set_render_dx(POP_CH_KID, dx); } /* ---- Порядок «Kid vs страж» (BUG-DRAWORDER-1) ----------------------- @@ -211,14 +212,14 @@ static uint8_t guard_over_kid(void) int kk, gk; if (Guard.charid == 0 || Guard.room != cur_room) return 0; objtile_at_char(&kr, &kc, &Kid, kid_cur_dx(), kid_cur_dy(), - kid_cur_flags(), kid_fp_width()); + kid_cur_flags(), pop_cd[POP_CH_KID].fpw); objtile_at_char(&gr, &gc, &Guard, pop_gframe.dx, pop_gframe.dy, - pop_gframe.flags, pop_guard_fp_width()); + pop_gframe.flags, pop_cd[POP_CH_OPP].fpw); kk = OBJ_ORDER_KEY(kr, kc); gk = OBJ_ORDER_KEY(gr, gc); if (gk != kk) return (uint8_t)(gk > kk); /* один тайл — ниже по obj_y рисуется позже (sort_curr_objs) */ - return (uint8_t)((int)Guard.y + pop_gframe.dy > kid_fp_obj_y()); + return (uint8_t)((int)Guard.y + pop_gframe.dy > pop_cd[POP_CH_KID].fpy); } /* Change-driven редрой ЛЕВОГО шва (ворота соседа room8 видны в col0): тяжёлый @@ -461,7 +462,6 @@ int main(void) * по умолчанию (без мерцания); SPACE переключает в однобуфер для * отладки багов рисования (edge по sp_prev, чтоб не дёргать). */ uint8_t front = 0, back, dbuf = 1, sp_prev = 0, frozen = 0; - uint8_t autofrz = 0; /* ВРЕМЕННО: автостоп на падении уже сработал */ uint16_t dead_frames = 0; #ifdef ROOMNAV uint8_t nav_prev = 0; @@ -502,7 +502,7 @@ int main(void) /* Фон комнаты рисуется в ОБЕ графические страницы: у каждой своя * ОЗУ-копия/видео-ОЗУ, а heal берёт чистый фон из ОЗУ-копии ТОЙ * страницы, в которую рисуем. Дальше каждый тик восстанавливаем - * ТОЛЬКО прямоугольник Кида (kid_heal), а не весь экран. */ + * ТОЛЬКО прямоугольник Кида (pop_char_heal), а не весь экран. */ /* Извлечь и отрисовать стартовую комнату (в обе страницы). P0: сбросить * trob/room_modif (ленивая инициализация из уровня при первом входе). */ /* Старт — ПО ДАННЫМ УРОВНЯ (комната/тайл/направление/поза входа), см. @@ -510,10 +510,8 @@ int main(void) * Shift шаг, ESC — выход. Карта коллизии задана внутри enter_room. */ pop_start_level(); gfx_set_draw_page(0); - kid_draw(); - pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), - kid_fp_width(), kid_fp_height(), - Kid.direction, Kid.frame, Kid.action); + pop_char_draw(POP_CH_KID); + pop_char_fore(POP_CH_KID); if (pop_ctrl_init() != 0) { /* raw-канал клавиатуры (забирает ввод у DSS) */ closegraph(); @@ -543,22 +541,6 @@ int main(void) * стоит, можно спокойно разбирать позу Kid и окклюзию. */ if (kbd_raw_down(0x16)) frozen = 1; if (kbd_raw_down(0x1E)) frozen = 0; - /* ВРЕМЕННО: автостоп на падении соперника в провал — сцену «скелет - * просвечивает сквозь кладку» руками не поймать (падение длится - * несколько кадров). Замирает сам, дальше снимок и чтение памяти. - * - * Срабатывает ОДИН раз на падение: пока стоит фриз, состояние - * персонажа не меняется, поэтому условие остаётся истинным — без - * этого флага снятие фриза клавишей '2' тут же гасилось повторным - * автостопом в той же итерации. Взводится заново, когда падение - * кончилось (страж убран или вышел из freefall). - * Снять вместе с разбором. */ - if (Guard.charid != 0 && Guard.action == 4 /* in_freefall */ && - Guard.y > 150) { - if (!autofrz) { frozen = 1; autofrz = 1; } - } else { - autofrz = 0; - } { /* Читы (seg000: switch под if (cheats_enabled)). Все — по * ФРОНТУ нажатия, иначе срабатывали бы каждый кадр. * K — убить стража; @@ -661,8 +643,8 @@ int main(void) pop_check_skel(); /* спецсобытие ур.3: скелет встаёт */ pop_check_can_guard_see_kid(); /* луч видимости — ДО логики персонажей */ pop_ctrl_tick(); /* ввод -> control(): смена seq */ - kid_heal(); /* стереть прошлый кадр ЭТОЙ страницы */ - pop_guard_heal(); /* и прошлый кадр стража */ + pop_char_heal(POP_CH_KID); /* стереть прошлый кадр ЭТОЙ страницы */ + pop_char_heal(POP_CH_OPP); /* и прошлый кадр соперника */ pop_fore_heal(); /* + следы оверлея (шире прямоугольника Kid) */ PROF(2); /* логика (тот же синий) */ kid_tick(); /* play_seq: следующий кадр */ @@ -680,7 +662,7 @@ int main(void) pop_fell_out = 0; if (down) { /* L2: переход в комнату снизу — продолжает падать */ enter_room(down); /* координаты Kid уже репроецированы pop_map */ - gfx_set_draw_page(back); /* вернуть текущую back для kid_draw */ + gfx_set_draw_page(back); /* вернуть текущую back для отрисовки */ } else { /* выпал из уровня (нет комнаты снизу) — рестарт */ pop_start_level(); gfx_set_draw_page(back); @@ -832,35 +814,31 @@ int main(void) update_kid_render_dx(); /* straddle: смещение спрайта, если kid_room≠cur_room */ PROF(6); /* спрайты: страж, Kid, fore поверх */ { /* Кто позже — тот поверх; порядок задаёт обход тайлов, а не роль - * персонажа (см. guard_over_kid). */ + * персонажа (см. guard_over_kid). Отрисовка одна на всех Char + * (pop_cdraw.c), различает их только слот. */ uint8_t g_after = guard_over_kid(); - if (!g_after) pop_guard_draw(); - PROF(6); /* kid_draw (тот же циан) */ - kid_draw(); /* нарисовать новый кадр (детект выхода за поле) */ - if (g_after) pop_guard_draw(); - /* Брызги стража рисует сам pop_guard_draw (порядок objtable), а - * флаг снимаем здесь: стража могло не оказаться в отрисованной - * комнате — тогда брызги просто пропадают, как в оригинале. - * Снимаем ПО ФАКТУ, а не безусловно: запись в кадре удара — - * готовая точка останова для отладки в MAME (watchpoint), и - * лишней записи каждый кадр нет. */ + if (!g_after) { pop_char_draw(POP_CH_OPP); pop_char_fore(POP_CH_OPP); } + PROF(6); /* кадр Кида (тот же циан) */ + pop_char_draw(POP_CH_KID); /* спрайт + брызги + клинок */ + if (g_after) { pop_char_draw(POP_CH_OPP); pop_char_fore(POP_CH_OPP); } + /* Брызги урона рисует сама отрисовка персонажа (порядок objtable: + * сразу после него и до клинка), а флаги снимаем здесь: соперника + * могло не оказаться в отрисованной комнате — тогда брызги просто + * пропадают, как в оригинале. Снимаем ПО ФАКТУ, а не безусловно: + * запись в кадре удара — готовая точка останова для отладки в + * MAME (watchpoint), и лишней записи каждый кадр нет. */ if (pop_guard_hurt) pop_guard_hurt = 0; + if (pop_kid_hurt) pop_kid_hurt = 0; } pop_loose_mob_draw_over(); /* падающая плита НИЖЕ Kid → поверх него (y-порядок) */ - if (pop_kid_hurt) { /* HP убавилось → «брызги» удара (draw_hurt_splash) */ - kid_draw_splash(); - pop_kid_hurt = 0; - } pop_hp_draw(); /* полоса HP — только при изменении */ PROF(6); /* fore поверх Kid + борта */ - /* Окно fore-клипа общее, и pop_guard_draw ставит своё. При «страж - * поверх» он отрабатывает ПОСЛЕ kid_draw, поэтому окно Кида - * восстанавливаем явно — иначе передние грани у Кида отсекаются и - * он рисуется поверх столбов. */ - kid_fore_clip_restore(); - pop_fore_over_kid(kid_fp_obj_x(), kid_fp_obj_y(), - kid_fp_width(), kid_fp_height(), - Kid.direction, Kid.frame, Kid.action); + /* Fore-слой Кида — В КОНЦЕ, после плиты и полосы HP: окно fore-клипа + * одно на всех, и его ставит каждый, кто рисовал персонажа, поэтому + * pop_char_fore(KID) заодно возвращает окно Кида (при «соперник + * поверх» в нём остался бы соперник, и передние грани у Кида + * отсеклись бы целиком — он рисовался бы поверх столбов). */ + pop_char_fore(POP_CH_KID); pop_room_clip_borders(); /* почистить борта — только в кадрах падения (гейт) */ /* Пейсинг 3 кадра, затем tear-free флип на vblank: draw уже diff --git a/applications/PoP/roomtest/tests-host/stubs.c b/applications/PoP/roomtest/tests-host/stubs.c index 2901f47..19e57bd 100644 --- a/applications/PoP/roomtest/tests-host/stubs.c +++ b/applications/PoP/roomtest/tests-host/stubs.c @@ -36,6 +36,7 @@ #include "pop_level.h" #include "pop_redraw.h" #include "_pop_draw.h" +#include "pop_cdraw.h" #include "stubs.h" #include "kidblob.h" @@ -195,10 +196,16 @@ void pop_heal_fast(int x, int y, int w, int h) uint8_t pop_current_level; /* pop_level.c под тест не линкуется; sc_init ставит 1 */ -/* Цвет стража — подмена палитры, живёт в pop_gdraw (отрисовка, в набор не +/* Цвет стража — подмена палитры, живёт в pop_cdraw (отрисовка, в набор не * линкуется). Здесь важен только факт вызова из pop_guard_enter. */ void pop_guard_set_palette(uint8_t color) __banked { (void)color; } +/* Габарит последнего нарисованного кадра по слотам (pop_cdraw.c): физика + * читает из него char_width_half. Под тестом никто не рисует, поэтому все + * поля остаются нулями — ровно как было до DRAW-CHAR, когда kid_fp_width() + * возвращал 0. Тест, которому ширина важна, выставляет её сам. */ +pop_cdraw_t pop_cd[POP_CH_N]; + /* Связи комнат: нужны check_guard_fallout (возрождение скелета в комнате * снизу). Тест задаёт их через tk_link_down. */ uint8_t tk_link_down; diff --git a/applications/PoP/toolchain/pop_extract_kid_data.py b/applications/PoP/toolchain/pop_extract_kid_data.py index de4f36b..c96f660 100644 --- a/applications/PoP/toolchain/pop_extract_kid_data.py +++ b/applications/PoP/toolchain/pop_extract_kid_data.py @@ -112,9 +112,14 @@ def main(): f.write("#define KID_BIN_GFRAMES_OFF %d\n\n" % (len(frames) * 5 + len(seqtbl))) # seq offsets + # kid_seq_off — МАКРО-инициализатором, не готовым массивом: заголовок + # включают почти все модули PoP, и `static const` размножал таблицу по + # каждому TU мёртвой копией (замер 2026-08-08: 230 Б × 9 модулей = + # 1840 Б впустую, в т.ч. 460 Б резидента W1/W2). Сам массив + # определяет ОДИН pop_kid.c — та же причина, что у sword_tbl ниже. f.write("#define KID_NSEQ %d\n" % len(offs)) - f.write("static const uint16_t kid_seq_off[KID_NSEQ] = {\n") - f.write(emit_c(offs, 8, "0x%04X") + "\n};\n\n") + f.write("#define KID_SEQ_OFF_INIT { \\\n") + f.write(emit_c(offs, 8, "0x%04X").replace("\n", " \\\n") + " \\\n}\n\n") # sword_tbl — макро-инициализаторами, а не готовыми массивами: так # 159 байт таблицы лягут в ОДИН TU (pop_kid.c), а не в каждый, кто