DRAW-CHAR: отрисовка одна на всех Char; разгрузка банка 2 (90.4% -> 72.9%)

DRAW-CHAR.  Отрисовка персонажа сведена к одному набору функций над Char —
как физика после GUARD-PHYS.  В оригинале add_kid_to_objtable (seg008:22F0) и
add_guard_to_objtable (seg008:2324) имеют идентичное тело и различаются
окном (loadkid/loadshad), набором спрайтов и типом объекта, а
redraw_at_char/redraw_at_char2 гейтов по charid не имеют вовсе.

  pop_gdraw.c -> pop_cdraw.c: pop_char_draw/heal/fore(who), слот
  POP_CH_KID / POP_CH_OPP; состояние слотов pop_cd[] в _DATA — читается из
  любого банка без трамплина.  Проход окклюзии тоже один
  (pop_fore_over_char), pop_fore_over_kid больше нет.

Починилось само (расхождения, которые и были ценой дублирования): у
соперника не было clip_char; у Кида не было клипа полем 192 и ветки брызг
«мёртв/падение»; char_width_half СТРАЖА считался по спрайту КИДА.

Замер: _CODE 24 881 -> 20 524 (куча 2023 -> 6333), BANK2 -265, итого -3.2 КБ.
Проверено пользователем в MAME; циан-полоса профиля подросла — оптимизация
заведена отдельной задачей DRAW-COST.

MEM-BANK2, шаг 1: общие «листья» слоя фона в РЕЗИДЕНТ (pop_tile.c/.h).
Ограничение платформы: писучие данные банка лежат в _DATA и видны всем, а
const-таблицы — в странице банка, из другого банка их не прочитать; трамплин
же выбирается объявлением, то есть __banked на листе бьёт и по горячим
вызывающим (654 такта).  W1 замаплено всегда — оттуда обе половины зовут
листья прямым call и читают таблицы напрямую.

MEM-BANK2, шаг 2: дедуп внутри банка.  wall_pattern 808 -> 394 и wall_rnd
786 -> 654: четыре ветки по виду стены отличались только набором кусков и
числами в одной серии prandom — сведены к таблицам WP_PARTS и WR_RULE,
порядок вызовов prandom сохранён дословно.

Заодно: kid_seq_off больше не static const в kid_data.h (230 Б мёртвой копии
в каждом из 9 модулей) — генератор pop_extract_kid_data.py отдаёт
макро-инициализатор, массив определяет один pop_kid.c.

Итог: BANK2 14 815 -> 11 942 (72.9 %, свободно 4442 Б), _CODE 22 556,
куча 4301 Б.  tests-host зелёные (65/39/53/1723/1); в MAME комната 1
совпала с дорефакторным снимком попиксельно (0 из 227 520), комната 3 —
та же раскладка кладки.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 13:25:53 +03:00
parent 6673279cef
commit bbf91d10ee
28 changed files with 1593 additions and 1471 deletions
+1 -1
View File
@@ -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),
+2 -2
View File
@@ -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` окно клипа
принадлежит Киду». Отдельный инструмент, дополняющий тесты.
## Порядок работ
+10 -2
View File
@@ -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, и это сняло
> главное ограничение резидента (из банка его было не достать) — банк→банк
> работает, трамплин сохраняет страницу окна на стеке. Отрисовка стража
+14 -7
View File
@@ -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`) поверх `<kbd_raw.h>`. |
| `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
## Порядок слоёв в кадре (важно для окклюзии)
`roomtest.c` каждый тик рисует в СКРЫТУЮ страницу: `kid_heal` (стереть
прошлый кадр) → `kid_tick``pop_phys_tick` `pop_loose_tick` (loose СЛОЙ
ФОНА — ДО Kid, чтобы Kid был поверх плиты) → `kid_draw``pop_fore_over_kid`
(передние грани тайлов поверх Kid) → `pop_room_clip_borders` (чистка бортов,
гейт по флагу — только в кадрах падения) → флип на vsync.
`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
берёт чистый фон из копии ТОЙ страницы, в которую рисуем. Любой динамический
+5 -5
View File
@@ -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)
+90 -1
View File
@@ -1,4 +1,4 @@
# roomtest — ЗАКРЫТЫЕ задачи (архив досок, обновлено 2026-08-07)
# roomtest — ЗАКРЫТЫЕ задачи (архив досок, обновлено 2026-08-08)
Сделанное — с протоколами замеров, граблями и причинами решений. Файл
существует не ради истории: половина записей ниже — это ЧИСЛА (сколько тактов
@@ -232,6 +232,95 @@ BUG-1 (провал на row 1 при переходе через открыты
## Отрисовка и память
<a id="draw-char"></a>
### 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)).
<a id="clip-1"></a>
### CLIP-1. Аудит блитов: где клип не нужен — **СДЕЛАНО 2026-08-01**
+107 -60
View File
@@ -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 @@
скриншотах пользователя). Обратная проверка: если в соседней комнате СВОЙ
живой страж — переход не происходит.
### <a id="mem-bank2"></a>MEM-BANK2. Разгрузка банка 2 — **шаги 1-2 сделаны 2026-08-08**
Банк 2 (`pop_bg.c`) упирался в потолок: 90.8 % ещё до чомперов. Разбор по
символам показал, что он держит ДВЕ разные вещи — горячий fore-проход (каждый
кадр) и холодную отрисовку тайлов (вход в комнату, точечные перерисовки), —
а общие «листья» (`blit_b`, `tile_code`, `tile_table`, `wall_pattern`) нужны
обеим.
**Ключевое ограничение платформы** (записано в memory
`sdcc_banked_call_rules`): писучие данные банка лежат в `_DATA`/W2 и видны
всем, а `const`-таблицы — в СТРАНИЦЕ банка, и из другого банка их не
прочитать вовсе. Плюс трамплин выбирается ОБЪЯВЛЕНИЕМ: пометить лист
`__banked` — значит заставить платить и горячих вызывающих (654 такта на
круг).
**Шаг 1 — общие листья в РЕЗИДЕНТ** (`pop_tile.c` + внутренний `pop_tile.h`).
W1 замаплен всегда, поэтому и горячая половина, и будущая холодная зовут их
прямым `call` и читают таблицы напрямую — без дублей и без трамплинов.
Уехали: `blit_b`, `env_b/wall_b/fore_b/pot_b`, `tile_code/tile_mod/
tile_code_drawn/wall_modifier`, `heal_off`, `get_loose_frame`,
`potion_flask`, `pop_fore_set_clip`, `pop_room_set_below/above` + таблицы
`tile_table`, `COL_XH`, `WALL_FRAM_*`, `SPIKES_FRAM_LEFT`,
`LOOSE_FRAM_LEFT/BOTTOM`. Заодно три функции перестали быть `__banked` —
минус трамплин на кадр.
**Шаг 2 — дедуп внутри банка.** `wall_pattern` (808 → 394) и `wall_rnd`
(786 → 654): четыре ветки по виду стены (SWS/SWW/WWS/WWW) отличались ТОЛЬКО
набором кусков и числами в одной и той же серии `prandom` — сведены к
таблицам `WP_PARTS` и `WR_RULE`. Порядок вызовов `prandom` сохранён
дословно (иначе разъедется раскладка кладки).
**Грабля, пойманная замером:** после выноса `tile_table` из `static const` в
глобальную SDCC перестала сворачивать `tile_table[10].fore_x/fore_y` в
константы и стала читать их в рантайме — `potion_flask` раздулся 115 → 265 Б.
Лечится макросами, которыми инициализируется та же запись таблицы (одно место
правки, дублирования данных нет): 265 → 230.
**Замер (ALLOCS=3000):**
```
было после ш.1 после ш.2
BANK2 14 815 12 460 11 942 (90.4 % -> 72.9 %, свободно 4442 Б)
_CODE 20 064 22 591 22 556 (куча 6793 -> 4301 Б)
```
С релизным `ALLOCS=100000` банк 2 будет ещё примерно на 500-600 Б меньше
(замер до разгрузки: 14 815 -> 14 176).
**Проверено:** `tests-host` зелёные; в MAME комната 1 уровня 1 после дедупа
совпала с дорефакторным снимком **попиксельно (0 различий из 227 520)**,
комната 3 (сплошная кладка, все четыре вида стены и марки) — та же раскладка
кирпича.
**Что осталось в запасе, если места снова не хватит** (не делаем, пока нет
причины): вынести ХОЛОДНУЮ половину (`draw_tile` 1800 + точечные
перерисовки 1438 + падающие плиты 1898 ≈ 5.1 КБ) в свободный банк 7. Теперь
это дёшево: листья уже в резиденте, а `wall_pattern`/`wall_rnd`/марки/
`draw_gate_back` нужны обеим половинам — им понадобится тонкая `__banked`-
обёртка (тело остаётся непомеченным, горячие вызывающие платят ноль).
### <a id="l3-chomp"></a>L3-CHOMP. Чомперы — механика уровня 3
**Где:** 5 штук, комнаты 5(тайл 4), 16(23,24,25), 22(26). Дальше они почти
@@ -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)).
### <a id="draw-char"></a>DRAW-CHAR. Отрисовка — ОДНА на всех Char, как физика после GUARD-PHYS
### <a id="draw-cost"></a>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` у физики.
### <a id="draw-char"></a>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). Оттуда же список того, что
надо потрогать вживую при приёмке уровней.
### <a id="l3-pass"></a>L3-PASS. Приёмка уровня 3 — обход всех комнат
+1 -1
View File
@@ -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
+17 -17
View File
@@ -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 = клинка нет.
File diff suppressed because it is too large Load Diff
+23 -29
View File
@@ -12,6 +12,7 @@
#define POP_BG_H
#include <stdint.h>
#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
+455
View File
@@ -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 <graphics.h>
#include <gfx.h>
#include <sprite.h>
#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), поэтому при флипе подаём xw. */
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();
}
}
+93
View File
@@ -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 <stdint.h>
#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
+4 -4
View File
@@ -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 <stdint.h>
#include <gfx.h>
-371
View File
@@ -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 <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_guard.h"
#include "pop_kid.h" /* pop_sword_draw — клинок общий с Кидом */
#include "pop_bg.h" /* POP_YOFF, pop_fore_over_char */
#include "_pop_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();
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
*
* Здесь только то, что должно оставаться достижимым из банка (guards.c):
* поля Guard, HP, вход в комнату, чит-убийство, чтение кадра из таблицы.
* ОТРИСОВКА в pop_gdraw.c, резиденте W3 (её зовёт лишь главный цикл).
* ОТРИСОВКА в pop_cdraw.c (банк 4), общая на всех Char.
*/
#include <stdint.h>
#include "pop_guard.h"
+5 -12
View File
@@ -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);
+24 -297
View File
@@ -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). */
+8 -32
View File
@@ -13,6 +13,7 @@
#include <stdint.h>
#include "pop_char.h"
#include "kid_data.h" /* kframe — тип кадра для pop_sword_draw */
#include <sprite.h> /* 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
+16 -7
View File
@@ -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; /* лицом вправо: левый край внутрь */
+3 -2
View File
@@ -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, правее которого спрайт не рисуется
+1 -1
View File
@@ -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);
/* Сбросить все пометки (смена комнаты/уровня). */
+240
View File
@@ -0,0 +1,240 @@
/*
* pop_tile.c общие ЛИСТЬЯ слоя фона: блит куска атласа, чтение тайла
* комнаты, таблица тайлов. РЕЗИДЕНТ W1 (_CODE).
*
* Мотивация раскладки (почему резидент, а не банк) в шапке pop_tile.h:
* коротко, эти функции и таблицы нужны ОБЕИМ половинам слоя фона (горячей
* fore-проход каждый кадр, холодной полная отрисовка комнаты), а код и
* rodata банка видны только пока банк замаплен. W1 замаплено всегда.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#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; }
+121
View File
@@ -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 <stdint.h>
#include <sprite.h>
#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
+2 -2
View File
@@ -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 не нужен. */
/* Створку рисует ПРАВАЯ половина двери; последний кадр анимации
* помечаем на обе страницы, иначе дверь дрожит через кадр. */
+32 -54
View File
@@ -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 уже
+8 -1
View File
@@ -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;
@@ -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), а не в каждый, кто